基于开源项目:基于FreeRTOS的stm32智能天气时钟复刻过程中的一些心得以及笔记,项目视频链接:
【【全开源手把手项目实战】基于FreeRTOS的stm32智能天气时钟】https://www.bilibili.com/video/BV1tfL1zeEQN?p=18&vd_source=f41ad9d8e6414afd88a3be14a0b309
IO基础
IO输出类型:
1、开漏输出(OD)

芯片内部只有一个NMOS管
NMOS无论导不导通:电压为0
为使芯片在开漏模式下有输出高电平能力,一般加一个上拉电阻

开漏输出优缺点
优点:
1.可实现电平转换,输出电平取决于上拉电阻电源;
2.可以实现IO的线与;
缺点:
1.高电平驱动能力差,取决于外部上拉电阻;
2.电平切换速率取决于外部上拉电阻,电阻越小速度越快;
2、推挽输出(PP)

同一时间只有一个管子(PMOS/NMOS)导通。
PMOS导通(推):Vout=VCC,输出外部高电平。
NMOS导通(挽):GND=Vout,外部流入低电平。
推挽输出优缺点:
优点:
1.输出高低电平与电源电压基本没有压差;
2.高低电平驱动能力较强,一般数字芯片推挽输出10口驱动电流最大可到20mA;
3.电平切换速度快。
缺点:不支持线与
3、开集输出(OC)

4、高阻态

E为使能信号,IN为缓冲器输入,OUT为缓冲器输出
1、当E使能,IN为高电平,OUT为高电平
2、当E使能,IN为低电平,OUT为低电平
3、当E失能,IN无论是高/低电平,OUT都不输出电平,称为高阻态
IO输入类型:
数字芯片的输入如果没有输入或者悬空,一般是高阻态,输入状态是未知的;
为了让数字芯片输入在没有输入或悬空时有确定的状态则就需要加上下拉电阻
上拉指芯片输入引脚通过电阻接到电源电压;
下上拉指芯片输入引脚通过电阻接到参考零电平;
在CMOS芯片(对电压变化敏感)上,为了防止静电造成损坏,不用的输入管脚不能悬空,一般需要接上下拉电阻;
上下拉电阻越大,我们称为强上拉和强下拉,静态电流越大;
上下拉电阻越小,我们称为弱上拉和弱下拉,静态电流越小;
1、上拉输入(IPU):将电平拉高

2、下拉输入(IPD):将电平拉低

中断
tips:使用中断时候一定一定一定不要忘了在中断函数里面清零标志位!!!
EXTI 外部中断基础配置
GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource0);
EXTI_InitTypeDef EXTI_InitStructure;
EXTI_InitStructure.EXTI_Line = EXTI_Line0;
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;
EXTI_InitStructure.EXTI_LineCmd = ENABLE;
EXTI_Init(&EXTI_InitStructure);
SYSTICK中断
main函数预览:
int main(void)
{
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB | RCC_AHB1Periph_GPIOE, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_SYSCFG, ENABLE);
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);
led_init();
led_all_off();
SysTick->LOAD = SystemCoreClock / 1000;
SysTick->VAL = 0;
SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk | SysTick_CTRL_TICKINT_Msk | SysTick_CTRL_ENABLE_Msk;
NVIC_SetPriority(SysTick_IRQn, 5);
while (1)
{
;
}
}
基础配置操作:
1、RCC时钟,GPIO/SYSCFG/SYSTICK?
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB | RCC_AHB1Periph_GPIOE, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_SYSCFG, ENABLE);
2、中断优先级组配置
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);
3、配置GPIO,实现LED闪烁,输出
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_StructInit(&GPIO_InitStructure);
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT;
GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_High_Speed;
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
GPIO_Init(GPIOE, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1;
GPIO_Init(GPIOB, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_Init(GPIOB, &GPIO_InitStructure);
4、配置SYSTICK,实现1ms计时
SysTick->LOAD = SystemCoreClock / 1000;
SysTick->VAL = 0;
SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk | SysTick_CTRL_TICKINT_Msk | SysTick_CTRL_ENABLE_Msk;
5、配置SYSTICK中断优先级5
NVIC_SetPriority(SysTick_IRQn, 5);
6、写SYSTICK中断服务函数,计数到500,gpio翻转,计数值归零
void SysTick_Handler(void)
{
static bool led_state = false;
static int count = 0;
if (++count >= 500)
{
count = 0;
led_state = !led_state;
led_set(1, led_state);
}
// 只用于清除外部中断标志位,SysTick标志位不需要手动清除
// NVIC_ClearPendingIRQ(SysTick_IRQn);
}
串口中断
USART常见中断解释:
一、接收相关中断
1️⃣ USART_IT_RXNE:Receive Not Empty(接收数据寄存器非空)
📌 触发条件:
收到 1 字节数据,DR 不为空
📌 作用:
最常用的接收中断
📌 典型用途:
串口接收数据、AT 指令通信、Modbus 接收
if (USART_GetITStatus(USART1, USART_IT_RXNE))
{
uint8_t data = USART_ReceiveData(USART1);
}
2️⃣ USART_IT_IDLE:
空闲中断
📌 触发条件:
一帧数据接收完成后总线空闲 1 帧时间
📌 作用:
判断“不定长数据帧结束”
📌 高级用法:
DMA + IDLE 判断一帧数据完成
⚠ 需要先读 SR 再读 DR 清除
二、发送相关中断
3️⃣ USART_IT_TXE:Transmit Data Register Empty(发送寄存器空)
📌 触发条件:
DR 数据被移入移位寄存器
📌 作用:
可以继续发送下一个字节
⚠ 不代表发送完成
4️⃣ USART_IT_TC:Transmission Complete(发送完成)
📌 触发条件:
移位寄存器 + 停止位发送完毕
📌 作用:
半双工、RS485 方向切换、发送结束标志
三、错误相关中断
5️⃣ USART_IT_PE:Parity Error(奇偶校验错误)
数据校验失败时触发
6️⃣ USART_IT_FE:Frame Error(帧错误)
停止位异常、通常波特率错误会出现
7️⃣ USART_IT_NE:Noise Error(噪声错误)
线路干扰
8️⃣ USART_IT_ORE_RX:Overrun Error(接收溢出)
📌 触发条件:
数据未读取,新数据到达
📌 影响:
会导致串口卡死
必须:读 SR、读 DR
9️⃣ USART_IT_ORE_ER:
也是溢出错误,但由 EIE 位控制。StdPeriph 兼容不同错误使能方式拆分定义。
🔟 USART_IT_ERR:
综合错误中断,包括:FE、NE、ORE,只要 CR3 的 EIE 使能
四、其他特殊中断
1️⃣1️⃣ USART_IT_LBD:
LIN Break Detection、用于 LIN 总线通信、普通串口基本不用
1️⃣2️⃣ USART_IT_CTS:Clear To Send
硬件流控 RTS/CTS 用,仅硬件流控模式有效
串口初始化模版:
USART_InitTypeDef USART_InitStruct;
USART_StructInit(&USART_InitStruct);
USART_InitStruct.USART_BaudRate = 115200;
USART_InitStruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
USART_InitStruct.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
USART_InitStruct.USART_Parity = USART_Parity_No;
USART_InitStruct.USART_StopBits = USART_StopBits_1;
USART_InitStruct.USART_WordLength = USART_WordLength_8b;
USART_Init(USART1, &USART_InitStruct);
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
USART_Cmd(USART1, ENABLE);
NVIC初始化模版:
NVIC_InitTypeDef NVIC_InitStruct;
NVIC_InitStruct.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 5;
NVIC_InitStruct.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStruct);
回调函数
小知识: typedef void (*func)(void);
void (*func)(void);
意思是:
定义一个“函数指针”,指向 无参数、无返回值的函数
表示:
- func 是一个指针
- 指向函数
- 函数返回值是 void
- 函数参数是 void
typedef void (*func)(void);
表示:
给这种函数指针类型起个别名叫
func
以后就可以写:
func myFunctionPointer;
而不是
void (*myFunctionPointer)(void);
void (*func)(void);
static void led1_toggle(void)
{
static bool state = false;
led_set(1, state);
state = !state;
}
func f;
f = led1_toggle;
f(); // 等价于 led1_toggle();
示例代码:
#include <stdint.h>
#include <string.h>
#include "stm32f4xx.h"
#include "led.h"
#include "key.h"
#include "usart.h"
#include "cpu_delay.h"
#define delay(ms) cpu_delay((ms) * 1000)
static void led1_toggle(void)
{
static bool state = false;
led_set(1, state);
state = !state;
}
static void led2_toggle(void)
{
static bool state = false;
led_set(2, state);
state = !state;
}
static void led3_toggle(void)
{
static bool state = false;
led_set(3, state);
state = !state;
}
int main(void)
{
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE);
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB, ENABLE);
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOC, ENABLE);
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOE, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_SYSCFG, ENABLE);
led_init();
key_init();
key_press_callback_register(1, led1_toggle);
key_press_callback_register(3, led3_toggle);
usart_init();
while(1)
{
;
}
}
#include <stdbool.h>
#include <stddef.h>
#include "stm32f4xx.h"
#include "key.h"
// KEY1: PA0
// KEY2: PC4
// KEY3: PC5
#define KEY1_PORT GPIOA
#define KEY1_PIN GPIO_Pin_0
#define KEY1_EXTI_PORT_SOURCE EXTI_PortSourceGPIOA
#define KEY1_EXTI_PIN_SOURCE EXTI_PinSource0
#define KEY1_EXTI_LINE EXTI_Line0
#define KEY1_IRQn EXTI0_IRQn
#define KEY2_PORT GPIOC
#define KEY2_PIN GPIO_Pin_4
#define KEY2_EXTI_PORT_SOURCE EXTI_PortSourceGPIOC
#define KEY2_EXTI_PIN_SOURCE EXTI_PinSource4
#define KEY2_EXTI_LINE EXTI_Line4
#define KEY2_IRQn EXTI4_IRQn
#define KEY3_PORT GPIOC
#define KEY3_PIN GPIO_Pin_5
#define KEY3_EXTI_PORT_SOURCE EXTI_PortSourceGPIOC
#define KEY3_EXTI_PIN_SOURCE EXTI_PinSource5
#define KEY3_EXTI_LINE EXTI_Line5
#define KEY3_IRQn EXTI9_5_IRQn
static key_func_t key1_func, key2_func, key3_func;
void key_init(void)
{
// GPIO初始化结构体模板
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_StructInit(&GPIO_InitStructure);
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN;
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_DOWN;
GPIO_InitStructure.GPIO_Speed = GPIO_Medium_Speed;
// EXTI中断初始化结构体模板
EXTI_InitTypeDef EXTI_InitStructure;
EXTI_StructInit(&EXTI_InitStructure);
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising;
EXTI_InitStructure.EXTI_LineCmd = ENABLE;
// EXTI中断优先级配置结构体模板(NVIC)
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 5;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
// KEY1初始化
GPIO_InitStructure.GPIO_Pin = KEY1_PIN;
GPIO_Init(KEY1_PORT, &GPIO_InitStructure);
SYSCFG_EXTILineConfig(KEY1_EXTI_PORT_SOURCE, KEY1_EXTI_PIN_SOURCE);
EXTI_InitStructure.EXTI_Line = KEY1_EXTI_LINE;
EXTI_Init(&EXTI_InitStructure);
NVIC_InitStructure.NVIC_IRQChannel = KEY1_IRQn;
NVIC_Init(&NVIC_InitStructure);
// KEY2初始化
GPIO_InitStructure.GPIO_Pin = KEY2_PIN;
GPIO_Init(KEY2_PORT, &GPIO_InitStructure);
SYSCFG_EXTILineConfig(KEY2_EXTI_PORT_SOURCE, KEY2_EXTI_PIN_SOURCE);
EXTI_InitStructure.EXTI_Line = KEY2_EXTI_LINE;
EXTI_Init(&EXTI_InitStructure);
NVIC_InitStructure.NVIC_IRQChannel = KEY2_IRQn;
NVIC_Init(&NVIC_InitStructure);
// KEY3初始化
GPIO_InitStructure.GPIO_Pin = KEY3_PIN;
GPIO_Init(KEY3_PORT, &GPIO_InitStructure);
SYSCFG_EXTILineConfig(KEY3_EXTI_PORT_SOURCE, KEY3_EXTI_PIN_SOURCE);
EXTI_InitStructure.EXTI_Line = KEY3_EXTI_LINE;
EXTI_Init(&EXTI_InitStructure);
NVIC_InitStructure.NVIC_IRQChannel = KEY3_IRQn;
NVIC_Init(&NVIC_InitStructure);
}
bool key_read(uint8_t idx)
{
switch (idx)
{
case 1: return GPIO_ReadInputDataBit(KEY1_PORT, KEY1_PIN) == Bit_SET;
case 2: return GPIO_ReadInputDataBit(KEY2_PORT, KEY2_PIN) == Bit_SET;
case 3: return GPIO_ReadInputDataBit(KEY3_PORT, KEY3_PIN) == Bit_SET;
default: return false;
}
}
void key_press_callback_register(uint8_t kidx, key_func_t func)
{
switch (kidx)
{
case 1: key1_func = func; break;
case 2: key2_func = func; break;
case 3: key3_func = func; break;
default: break;
}
}
void EXTI0_IRQHandler(void)
{
EXTI_ClearITPendingBit(KEY1_EXTI_LINE);
if (key1_func != NULL)
{
key1_func();
}
}
void EXTI4_IRQHandler(void)
{
EXTI_ClearITPendingBit(KEY2_EXTI_LINE);
if (key2_func != NULL)
{
key2_func();
}
}
void EXTI9_5_IRQHandler(void)
{
EXTI_ClearITPendingBit(KEY3_EXTI_LINE);
if (key3_func != NULL)
{
key3_func();
}
}
#ifndef __KEY_H__
#define __KEY_H__
#include <stdbool.h>
#include <stdint.h>
typedef void (*key_func_t)(void);
typedef void (*func)(void);
void key_init(void);
bool key_read(uint8_t idx);
void key_press_callback_register(uint8_t kidx, key_func_t func);
#endif /* __KEY_H__ */
面向对象+回调函数
知识点:##和#的区别
【#符号的功能和实例】
#include <stdio.h>
#define PRINT(NAME) printf(#NAME)
int main()
{
PRINT (Hello world 1);
printf("\r\n");
printf("Hello world 2");
}
return 0;
Hello world 1
Hello world 2
在宏定义中,#符号将紧随其后的参数视为字符串处理,其作用类似于对该宏变量添加双引号。具体来说,
#define PRINT(NAME) printf(#NAME)这个宏的定义,使得在调用时,括号内的内容会被直接打印出来,就如同被双引号包裹一样。
【##符号的功能和实例】
在宏定义中,##符号被用作连接符,用于将两个宏变量拼接成一个整体。这种机制使得我们能够轻松地将不同的宏变量组合在一起,以创建更复杂的宏定义。
Timer定时器
基本步骤
1.开启时钟RCC_APBxPeriphClockCmd(RCC_APBxPeriph_TIMx, ENABLE)
2.初始化定时器TIM_TimeBaseInit(TIMx, &TIM_TimeBaseInitStruct)
3.NVIC初始化配置中断
4.使能定时器中断TIM_Cmd(ENABLE)
5.开启定时器TIM_Cmd(ENABLE)
Timer初始化模版
Timer基本初始化:
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStruct;
TIM_TimeBaseStructInit(&TIM_TimeBaseInitStruct);
TIM_TimeBaseInitStruct.TIM_Prescaler = 84 - 1;// 预分频
TIM_TimeBaseInitStruct.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseInitStruct.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInitStruct.TIM_Period = 1000 - 1;// ARR自动重装载值
TIM_TimeBaseInit(TIM6, &TIM_TimeBaseInitStruct);
NVIC_InitTypeDef NVIC_InitStruct;
NVIC_InitStruct.NVIC_IRQChannel = TIM6_DAC_IRQn;
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 5;
NVIC_InitStruct.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStruct);
TIM_ITConfig(TIM6, TIM_IT_Update, ENABLE);
TIM_Cmd(TIM6, ENABLE);
Timer PWM输出
GPIO配置:
GPIO_InitTypeDef GPIO_InitStruct;
GPIO_StructInit(&GPIO_InitStruct);
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF;
GPIO_InitStruct.GPIO_OType = GPIO_OType_PP;
GPIO_InitStruct.GPIO_PuPd = GPIO_PuPd_NOPULL;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_100MHz;
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_9;
GPIO_Init(GPIOE, &GPIO_InitStruct);
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1;
GPIO_Init(GPIOB, &GPIO_InitStruct);
GPIO_PinAFConfig(GPIOE, GPIO_PinSource9, GPIO_AF_TIM1);
GPIO_PinAFConfig(GPIOB, GPIO_PinSource0, GPIO_AF_TIM3);
GPIO_PinAFConfig(GPIOB, GPIO_PinSource1, GPIO_AF_TIM3);
Timer配置:
// 使用SDK原生时钟频率
RCC_ClocksTypeDef RCC_ClocksStruct;
RCC_GetClocksFreq(&RCC_ClocksStruct);
// 高速 APB2 域的最大允许频率为 84 MHz
uint32_t apb2_timclk_Mhz = RCC_ClocksStruct.PCLK2_Frequency / 1000 / 1000 * 2;
// 低速 APB1 域的最大允许频率为 42 MHz
uint32_t apb1_timclk_Mhz = RCC_ClocksStruct.PCLK1_Frequency / 1000 / 1000 * 2;
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStruct;
TIM_TimeBaseStructInit(&TIM_TimeBaseInitStruct);
TIM_TimeBaseInitStruct.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseInitStruct.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInitStruct.TIM_Period = 1000 - 1;// 自动重装载寄存器 ARR(Auto-Reload Register)的值,定时器计数到多少产生一次更新事件(溢出中断)
TIM_TimeBaseInitStruct.TIM_Prescaler = apb2_timclk_Mhz - 1;// 1Mhz:1us计数一次,TIM_Period从0-999,计数1000次,代表1ms,周期为1ms
TIM_TimeBaseInit(TIM1, &TIM_TimeBaseInitStruct);
TIM_TimeBaseInitStruct.TIM_Prescaler = apb1_timclk_Mhz - 1;
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseInitStruct);
// TIM_Output_Compare_and_PWM_modes
TIM_OCInitTypeDef TIM_OCInitStruct;
TIM_OCStructInit(&TIM_OCInitStruct);
TIM_OCInitStruct.TIM_OCMode = TIM_OCMode_PWM1;
TIM_OCInitStruct.TIM_OutputState = TIM_OutputState_Enable;
TIM_OCInitStruct.TIM_Pulse = 500 - 1; // 计数1000次,占一半周期,占空比为50%
TIM_OCInitStruct.TIM_OCPolarity = TIM_OCPolarity_High;
TIM_OC1Init(TIM1, &TIM_OCInitStruct);
TIM_OC3Init(TIM3, &TIM_OCInitStruct);
TIM_OC4Init(TIM3, &TIM_OCInitStruct);
TIM_OC1PreloadConfig(TIM1, TIM_OCPreload_Enable);
TIM_OC3PreloadConfig(TIM3, TIM_OCPreload_Enable);
TIM_OC4PreloadConfig(TIM3, TIM_OCPreload_Enable);
TIM_CtrlPWMOutputs(TIM1, ENABLE);
TIM_Cmd(TIM1, ENABLE);
TIM_Cmd(TIM3, ENABLE);
呼吸灯循环实例:
int led_pwm_duty = 500 - 1;
int led_pwm_duty_cnt = 1;
while(1)
{
cpu_delay(1000);
led_pwm_duty += led_pwm_duty_cnt;
if (led_pwm_duty >= 1000 - 1 || led_pwm_duty <= 0)
led_pwm_duty_cnt = -led_pwm_duty_cnt;
TIM_SetCompare1(TIM1, led_pwm_duty);
TIM_SetCompare3(TIM3, led_pwm_duty);
TIM_SetCompare4(TIM3, led_pwm_duty);
}
Timer 输入补获
输入补获基本配置:
// 配置中断内核属性
TIM_ICInitTypeDef TIM_ICInitStructure;
TIM_ICStructInit(&TIM_ICInitStructure);
TIM_ICInitStructure.TIM_Channel = TIM_Channel_1;
TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising;
TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI;
TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1;
TIM_ICInitStructure.TIM_ICFilter = 0x0;
// 这里使用的是TIM_PWMIConfig而不是TIM_GetCapturex(TIMx),因此要配TIM_SelectInputTrigger、TIM_SelectSlaveMode、TIM_SelectMasterSlaveMode
TIM_PWMIConfig(TIM3, &TIM_ICInitStructure);
TIM_SelectInputTrigger(TIM3, TIM_TS_TI1FP1);
TIM_SelectSlaveMode(TIM3, TIM_SlaveMode_Reset);
TIM_SelectMasterSlaveMode(TIM3,TIM_MasterSlaveMode_Enable);
TIM_Cmd(TIM3, ENABLE);
// 配制TIM3中断,使用TIM3_IRQHandler
TIM_ITConfig(TIM3, TIM_IT_CC1, ENABLE);
TIM3_IRQHandler服务函数:
void TIM3_IRQHandler(void)
{
RCC_ClocksTypeDef RCC_Clocks;
RCC_GetClocksFreq(&RCC_Clocks);
uint32_t tim3_apb1_clk_hz = RCC_Clocks.PCLK1_Frequency * 2ul;
TIM_ClearITPendingBit(TIM3, TIM_IT_CC1);
uint32_t ic1 = TIM_GetCapture1(TIM3) + 1;
uint32_t ic2 = TIM_GetCapture2(TIM3) + 1;
if (ic1 != 0)
{
duty_cycle = (ic2 * 100) / ic1;
frequency = tim3_apb1_clk_hz / ic1;
}
else
{
duty_cycle = 0;
frequency = 0;
}
}
I2C与EEPROM
I2C读写数据流程总览:

主机写模式:

主机发出起始信号位,随后发出读写标志位为0(Write),从机发送ACK回应;主机写入从机的地址位置,从机发送ACK回应;主机开始发送数据,从机发送ACK回应;主机产生停止信号,结束通信。
主机读模式:

首先主机发送起始位,然后先发出写信号,主机发出确认要读的从设备的地址寄存器位置;主机再次发送起位,这次发出读信号,从机回复ACK,从机开始向主机发数据,主机回答ACK,最后主机产生I2C_GenerateSTOP(),主机读完毕。
器件地址:
为了区分一个I2C上挂载的不同设备,I2C协议规定,第一组发送的8位数据,前七位为器件地址,范围为000 0000~111 1111(0x00~0x7F);最后一位为读/写信号。

I²C主机通信完整事件流程
主机发送数据时,事件顺序固定:
EV5 → EV6 → EV8 → EV8_2
START->EV5->发送地址->EV6->发送数据->EV8->发送完成->EV8_2->STOP
1️⃣ EV5 — 主机模式建立
I2C_EVENT_MASTER_MODE_SELECT
宏定义:((uint32_t)0x00030001)
SB Start Bit
MSL Master Mode
BUSY 总线忙
含义:START 信号已经发送成功,I2C进入主机模式
I2C_GenerateSTART(I2C1, ENABLE);
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT));
2️⃣ EV6 — 地址发送成功
主机发送地址后等待:从机ACK
事件:I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED
标志位:
ADDR 地址匹配
TRA 发送模式
TXE 发送寄存器空
BUSY
MSL
含义:从机已经响应,主机进入发送模式
I2C_Send7bitAddress(I2C1, addr, I2C_Direction_Transmitter);
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED));
3️⃣ EV7 — 主机接收数据
用于 主机接收模式:I2C_EVENT_MASTER_BYTE_RECEIVED
标志:RXNE = 1
含义:接收到1字节数据
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED));
data = I2C_ReceiveData(I2C1);
4️⃣ EV8 — 数据发送中
事件:I2C_EVENT_MASTER_BYTE_TRANSMITTING
标志:TXE = 1
含义:数据已经写入DR,正在发送
I2C_SendData(I2C1, data);
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTING));
5️⃣ EV8_2 — 数据发送完成
事件:I2C_EVENT_MASTER_BYTE_TRANSMITTED
标志:
TXE = 1
BTF = 1
含义:数据已经完全发送到总线
这个通常用于:最后一个字节发送完成,然后发 STOP。
6️⃣ 主机发送完整流程
标准 STM32 I²C 写流程:
I2C_GenerateSTART(I2C1, ENABLE);
while(!I2C_CheckEvent(I2C1, EV5));
I2C_Send7bitAddress(I2C1, addr, I2C_Direction_Transmitter);
while(!I2C_CheckEvent(I2C1, EV6));
I2C_SendData(I2C1, data);
while(!I2C_CheckEvent(I2C1, EV8));
I2C_GenerateSTOP(I2C1, ENABLE);
I²C从机通信完整事件流程
一、从机接收模式(Master → Slave)
当主机向从机写数据时,从机会经历以下事件:
1️⃣ EV1:从机地址匹配
事件:I2C_EVENT_SLAVE_RECEIVER_ADDRESS_MATCHED
含义:
- 主机发送 START + 地址
- 地址与从机地址匹配
- 从机进入接收模式
寄存器状态:
ADDR = 1
TRA = 0
2️⃣ EV2:接收到数据
事件:I2C_EVENT_SLAVE_BYTE_RECEIVED
含义:
- 主机发送数据
- 从机接收到一个字节
寄存器状态:RXNE = 1
处理:uint8_t data = I2C_ReceiveData(I2C1);
这个事件会 循环出现,直到主机停止发送。
3️⃣ EV4:停止信号检测
事件:I2C_EVENT_SLAVE_STOP_DETECTED
含义:
- 主机发送 STOP
- 从机通信结束
寄存器状态:STOPF = 1
处理:I2C_GetFlagStatus(I2C1, I2C_FLAG_STOPF);
二、从机发送模式(Slave → Master)
当主机读取从机数据时:
1️⃣ EV1:地址匹配(发送模式)
事件:I2C_EVENT_SLAVE_TRANSMITTER_ADDRESS_MATCHED
含义:
- 主机发送
START + 地址 + Read
寄存器状态:
ADDR = 1
TRA = 1
说明:
从机被请求发送数据。
2️⃣ EV3:数据寄存器空
事件:I2C_EVENT_SLAVE_BYTE_TRANSMITTING
含义:TXE = 1
说明:
数据寄存器空,需要发送数据。
代码:
I2C_SendData(I2C1, data);
3️⃣ EV3-2:数据发送完成
事件:I2C_EVENT_SLAVE_BYTE_TRANSMITTED
寄存器状态:
TXE = 1
BTF = 1
说明:
- 数据已经发送
- 可以发送下一字节
三、完整从机事件流程图
START
↓
EV1
地址匹配
↓
EV2
接收数据
↓
EV2
接收数据
↓
EV2
接收数据
↓
EV4
STOP
START
↓
EV1
地址匹配
↓
EV3
发送数据
↓
EV3
发送数据
↓
EV3-2
发送完成
↓
STOP
四、典型从机代码框架
while (1)
{
if(I2C_CheckEvent(I2C1, I2C_EVENT_SLAVE_RECEIVER_ADDRESS_MATCHED))
{
// 地址匹配
}
if(I2C_CheckEvent(I2C1, I2C_EVENT_SLAVE_BYTE_RECEIVED))
{
data = I2C_ReceiveData(I2C1);
}
if(I2C_CheckEvent(I2C1, I2C_EVENT_SLAVE_STOP_DETECTED))
{
// 通信结束
}
}
I²C 起始条件:
- 起始位、地址位:SCL不动,SDA进入0表示工作状态
- 应答位:SDA的接收方回应发送方已收到数据的标志
- 数据位:SDA的发送方修改SDA状态
- 主从方、发送接收方关系:SCL主机控制,SDA发送方控制
SDA : 1 → 0
SCL : 1

I2C基础代码:
// 根据官方文档:Program the Mode, duty cycle , Own address, Ack, Speed and Acknowledged Address using the I2C_Init() function.
I2C_InitTypeDef I2C_InitStruct;
I2C_StructInit(&I2C_InitStruct);
I2C_InitStruct.I2C_Mode = I2C_Mode_I2C;
I2C_InitStruct.I2C_DutyCycle = I2C_DutyCycle_2;
I2C_InitStruct.I2C_OwnAddress1 = 0x00;
I2C_InitStruct.I2C_Ack = I2C_Ack_Enable;
I2C_InitStruct.I2C_ClockSpeed = 100ul * 1000ul;
I2C_InitStruct.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;
I2C_Init(I2C1, &I2C_InitStruct);
// STM32 I²C 主机发送一个字节完整流程
I2C_AcknowledgeConfig (I2C1, ENABLE)
I2C_GenerateSTART (I2C1, ENABLE)// 产生起始信号
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT));// 等待主模式建立
I2C_Send7bitAddress (I2C1, OXAB, I2C_Direction_Transmitter);// 发送从机地址
while (!I2C_CheckEvent (I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED))// 等待地址应答
I2C_SendData (I2C1, 0x00);// 发送数据
while (!I2C_CheckEvent (I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTING));// 等待数据发送完成
I2C_GenerateSTOP (I2C1, ENABLE);// 发送 STOP 信号
I2C_AcknowledgedAddress_7bit为8位数据位,前7位为1 0 1 0 A2 A1 A1,
最后一位0为W(write),1为R(read)
宏函数:
如果 硬件出错:
- 没有从机
- 总线被拉低
- SDA 被占用
程序就会:死循环/CPU卡死
所以要增加超时机制
#define I2C_CHECK_EVENT(EVENT, TIMEOUT) \
do { \
uint32_t timeout = TIMEOUT; \
while (!I2C_CheckEvent(I2C1, EVENT) && timeout > 0) { \
cpu_delay(10); \
timeout -= 10; \
} \
if (timeout <= 0) \
return false; \
} while (0)
// 把宏参数复制到局部变量。
uint32_t timeout = TIMEOUT;
do {
…
} while (0)
这是 宏编程常见技巧。
作用:保证宏像一条语句一样使用
if(x)
I2C_CHECK_EVENT(...);
else
...
