<think>我们使用STM32F407VET6的HAL库驱动三个超声波模块(HC-SR04),不使用Delay函数,采用非阻塞方式。
方案:使用一个定时器(如TIM2)的三个通道来捕获Echo引脚的高电平时间(输入捕获),使用另外的GPIO作为Trig引脚(普通输出)。
具体引脚分配:
- 中间:Trig = PD7, Echo = PD6 (使用TIM2_CH1捕获PD6,因为PD6对应TIM2_CH1)
- 右边:Trig = PB12, Echo = PB14 (注意:PB14对应TIM1_CH2,但我们不使用TIM1,因为TIM1是高级定时器,配置复杂。我们可以使用TIM8_CH2,但PB14也可以映射到其他定时器?查资料:PB14可以映射到TIM12_CH1,但TIM12在APB1上,且只有一个通道。因此我们考虑使用多个定时器或者使用同一个定时器的不同通道。但三个Echo引脚需要三个捕获通道。
查STM32F407VET6的定时器通道与引脚对应关系:
- PD6 -> TIM2_CH1 (可用)
- PB14 -> TIM1_CH2 (高级定时器) 或者 TIM8_CH2 (也是高级定时器) 或者 TIM12_CH1 (APB1)
- PB15 -> TIM1_CH3 或者 TIM8_CH3 或者 TIM12_CH2
由于我们使用HAL库,并且希望使用一个定时器来捕获三个通道,我们可以选择:
- TIM2:有4个通道,但PD6(CH1)、PD7(CH2?但PD7已经被用作Trig,不能同时用作捕获)——注意:Trig和Echo是分开的,所以PD6作为捕获,PD7作为普通输出。
- 对于PB14和PB15,我们可以使用另一个定时器。但是为了简化,我们使用两个定时器:TIM2(2个通道)和TIM3(2个通道)?或者使用TIM5(4个通道)?因为TIM5的通道0到3对应PA0~PA3,但我们可以重映射吗?或者使用其他引脚?
实际上,我们需要查看引脚定义:
- PB14:可以配置为TIM1_CH2(但TIM1在APB2上)或者TIM8_CH2(也在APB2)或者TIM12_CH1(APB1)。由于我们使用三个超声波,我们可以使用两个定时器:比如TIM2(APB1)和TIM12(APB1)?或者使用一个定时器的多个通道?但TIM2只有4个通道,并且PD6(TIM2_CH1)已经用了一个,还有三个通道可用(PA1, PA2, PA3)?但我们的引脚是PB14和PB15。
因此,我们需要重新考虑引脚配置,或者使用定时器重映射功能。但是,为了简化,我们建议使用三个定时器的输入捕获通道,但这样会占用多个定时器资源。另一种方法是使用一个定时器,但需要将Echo引脚配置为同一个定时器的不同通道。
根据STM32F407VET6的引脚定义,我们可以选择:
- 中间:PD6(TIM2_CH1)
- 右边:PB14(TIM12_CH1)
- 左边:PB15(TIM12_CH2)
这样,我们可以用TIM2处理中间的超声波,用TIM12处理左右两个超声波。但是这样需要两个定时器。
我们也可以考虑使用TIM5(32位定时器,有4个通道):
- TIM5_CH1: PA0
- TIM5_CH2: PA1
- TIM5_CH3: PA2
- TIM5_CH4: PA3
但我们的Echo引脚分别是PD6、PB14、PB15,这些引脚都不能直接映射到TIM5。因此,我们只能放弃使用同一个定时器。
我们决定:
- 中间的Echo(PD6)使用TIM2_CH1
- 右边的Echo(PB14)使用TIM12_CH1
- 左边的Echo(PB15)使用TIM12_CH2
这样,我们需要配置两个定时器:TIM2和TIM12。
步骤:
1. 配置三个Trig引脚为输出(PD7, PB12, PD8)——注意:左边的Trig是PD8,Echo是PB15。
2. 配置三个Echo引脚为输入,并分别配置到对应的定时器通道(PD6->TIM2_CH1, PB14->TIM12_CH1, PB15->TIM12_CH2)
3. 配置TIM2和TIM12为输入捕获模式,上升沿和下降沿都捕获(这样可以在中断中计算高电平时间)
4. 使用非阻塞方式触发超声波模块(分时触发,避免干扰)
5. 在定时器捕获中断中记录时间并计算距离
注意:TIM2挂在APB1上,时钟频率为84MHz(假设系统时钟168MHz,APB1分频系数为2,所以定时器时钟为84MHz);TIM12也挂在APB1上,同样84MHz。
配置步骤(HAL库):
- 初始化Trig引脚(PD7, PB12, PD8)为推挽输出,初始低电平。
- 初始化Echo引脚(PD6, PB14, PB15)为输入,复用为对应的定时器通道。
- 初始化TIM2和TIM12:
定时器时钟84MHz,预分频设置为84-1,这样计数频率为1MHz(1计数=1us)。
自动重载值设为最大值(0xFFFFFFFF或0xFFFF,TIM2是32位,TIM16是16位,TIM12是16位,所以TIM2的ARR=0xFFFFFFFF,TIM12的ARR=0xFFFF)。
配置通道为输入捕获模式,双边沿触发。
使能捕获中断和更新中断(更新中断用于溢出处理,可选)。
启动定时器。
- 在定时器的捕获中断回调函数中,根据捕获的边沿记录时间,并计算高电平持续时间。
触发逻辑:
为了避免三个超声波同时触发相互干扰,我们采用分时触发:先触发中间的,等待一段时间(比如20ms)后触发右边的,再等待20ms触发左边的,然后循环。
注意:触发信号只需要10us的高电平。我们可以使用一个定时器(如TIM6)来产生时基,每隔20ms触发一个超声波,轮流触发。
这里我们使用TIM6(基本定时器)来产生20ms的时基,在TIM6的中断中依次触发不同的超声波模块。
具体步骤:
1. 配置TIM6为20ms中断(84MHz/84000/20=84MHz分频8400,计数200次?84MHz/8400=10kHz,10kHz的2000个计数是200ms?所以需要调整)
预分频值=8400-1,则计数器频率=84MHz/8400=10kHz,即每计数一次0.1ms。要20ms,则计数200次。所以ARR=200-1。
2. 在TIM6的中断中,设置一个状态机,每次中断切换要触发的超声波模块。
state=0: 触发中间的
state=1: 触发右边的
state=2: 触发左边的
然后state循环。
3. 触发函数:在中断中,将对应的Trig引脚拉高,然后设置一个标志,并在很短的时间后(10us)拉低。但是我们不能用延时,所以我们可以在定时器中断中设置Trig引脚为高,然后启动一个一次性的定时器(比如用TIM7)在10us后产生中断来拉低Trig。但这样需要额外定时器。
4. 另一种方法:在TIM6中断中,我们设置Trig引脚为高,并记录当前时间(使用TIM6的计数器),然后在主循环中检查时间,当超过10us后拉低。但是这样不是实时的。
5. 更简单的方法:由于10us非常短,我们可以使用一个循环来产生10us的延时。但注意,系统时钟168MHz,10us就是1680个周期,我们可以用循环精确延时。但是,这样在中断中执行延时会阻塞,不过10us很短,可以接受。
因此,我们在TIM6中断中这样处理:
state_machine:
0:
PD7 = 1; // 触发中间的
delay_us(12); // 延时12us
PD7 = 0;
state = 1;
1:
PB12 = 1;
delay_us(12);
PB12 = 0;
state = 2;
2:
PD8 = 1;
delay_us(12);
PD8 = 0;
state = 0;
注意:这个delay_us会阻塞中断12us,但总的中断时间大约12*3=36us,而20ms一次中断,所以占用率很低(0.18%),可以接受。
6. 在输入捕获中断中处理Echo信号:
对于每个超声波,在捕获中断中:
如果是上升沿,则记录当前捕获值(作为开始时间),并改变捕获边沿为下降沿。
如果是下降沿,则记录当前捕获值,计算时间差(当前值减去开始时间),即为高电平时间,然后转换为距离(距离=时间*340/2/10000,单位厘米,因为时间单位是us,340m/s=340*100/1000000 cm/us = 0.034cm/us,所以距离=时间*0.034/2=时间*0.017 cm)
7. 注意定时器溢出:如果距离很远,定时器可能会溢出。我们需要处理溢出中断,在溢出中断中记录溢出次数。但这里我们使用16位或32位定时器,最大计数时间:对于16位定时器(TIM12),最大计时65535us=65ms,对应距离65*0.017=1.1米,而超声波最大测量距离通常为4米,所以不够。因此,我们需要使用32位定时器(TIM2)或者扩展计数(使用更新中断记录溢出次数)。
由于TIM12是16位的,我们需要处理溢出。我们可以这样:
在定时器的更新中断中,让一个计数器(比如overflow_count)加1。
在捕获中断中,如果发生上升沿,则重置overflow_count为0,并记录当前捕获值和当前overflow_count(作为开始计数)。
在下降沿,记录当前捕获值和overflow_count,然后计算总时间 = (overflow_count * 65536 + 当前捕获值) - 开始捕获值(注意,开始捕获值对应的overflow_count为0,如果期间有溢出,则overflow_count就是溢出的次数)
但是,因为我们的定时器频率为1MHz,所以每个计数为1us。对于最大4米(约23.5ms=23500us),所以对于16位定时器,最大计数65535us>23500us,所以不会溢出。因此,我们设置自动重载值为0xFFFF(最大值),在4米以内不会溢出,可以忽略溢出处理。
8. 在中断回调函数中,我们计算距离并存储到全局变量中。
代码结构:
全局变量:
float distance_mid = 0.0; // 中间距离
float distance_right = 0.0; // 右边距离
float distance_left = 0.0; // 左边距离
定时器配置:
步骤:
1. 配置Trig引脚:PD7, PB12, PD8 为输出
2. 配置Echo引脚:PD6, PB14, PB15 为复用模式,并映射到对应的定时器通道
PD6 -> TIM2_CH1
PB14 -> TIM12_CH1
PB15 -> TIM12_CH2
3. 配置TIM2:输入捕获,通道1
4. 配置TIM12:输入捕获,通道1和通道2
5. 配置TIM6:20ms中断,用于触发超声波
具体代码:
注意:使用HAL库,我们需要重写捕获中断回调函数。
初始化函数:
</think>
### STM32F407VET6驱动三个超声波模块的非阻塞实现(HAL库)
#### 硬件配置
```c
// 超声波引脚定义
#define US_MID_TRIG_PIN GPIO_PIN_7
#define US_MID_TRIG_PORT GPIOD
#define US_MID_ECHO_PIN GPIO_PIN_6
#define US_MID_ECHO_PORT GPIOD // TIM2_CH1
#define US_RIGHT_TRIG_PIN GPIO_PIN_12
#define US_RIGHT_TRIG_PORT GPIOB
#define US_RIGHT_ECHO_PIN GPIO_PIN_14
#define US_RIGHT_ECHO_PORT GPIOB // TIM12_CH1
#define US_LEFT_TRIG_PIN GPIO_PIN_8
#define US_LEFT_TRIG_PORT GPIOD
#define US_LEFT_ECHO_PIN GPIO_PIN_15
#define US_LEFT_ECHO_PORT GPIOB // TIM12_CH2
// 全局变量
volatile uint32_t rise_time[3] = {0};
volatile float distances[3] = {0}; // 存储三个距离值
```
#### 定时器配置(TIM2和TIM12)
```c
void MX_TIM2_Init(void) {
TIM_IC_InitTypeDef sConfigIC = {0};
htim2.Instance = TIM2;
htim2.Init.Prescaler = 84-1; // 84MHz/84 = 1MHz (1计数=1μs)
htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
htim2.Init.Period = 0xFFFFFFFF; // 32位最大值
htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
HAL_TIM_IC_Init(&htim2);
// 配置通道1(PD6)
sConfigIC.ICPolarity = TIM_INPUTCHANNELPOLARITY_BOTHEDGE;
sConfigIC.ICSelection = TIM_ICSELECTION_DIRECTTI;
sConfigIC.ICPrescaler = TIM_ICPSC_DIV1;
sConfigIC.ICFilter = 6; // 滤波系数
HAL_TIM_IC_ConfigChannel(&htim2, &sConfigIC, TIM_CHANNEL_1);
HAL_TIM_IC_Start_IT(&htim2, TIM_CHANNEL_1); // 启动捕获中断
}
void MX_TIM12_Init(void) {
TIM_IC_InitTypeDef sConfigIC = {0};
htim12.Instance = TIM12;
htim12.Init.Prescaler = 84-1; // 1MHz计数
htim12.Init.CounterMode = TIM_COUNTERMODE_UP;
htim12.Init.Period = 0xFFFF; // 16位最大值
htim12.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
HAL_TIM_IC_Init(&htim12);
// 配置通道1(PB14)
sConfigIC.ICPolarity = TIM_INPUTCHANNELPOLARITY_BOTHEDGE;
sConfigIC.ICSelection = TIM_ICSELECTION_DIRECTTI;
sConfigIC.ICPrescaler = TIM_ICPSC_DIV1;
sConfigIC.ICFilter = 6;
HAL_TIM_IC_ConfigChannel(&htim12, &sConfigIC, TIM_CHANNEL_1);
// 配置通道2(PB15)
sConfigIC.ICSelection = TIM_ICSELECTION_INDIRECTTI;
HAL_TIM_IC_ConfigChannel(&htim12, &sConfigIC, TIM_CHANNEL_2);
HAL_TIM_IC_Start_IT(&htim12, TIM_CHANNEL_1);
HAL_TIM_IC_Start_IT(&htim12, TIM_CHANNEL_2);
}
```
#### 输入捕获中断处理
```c
// TIM2中断回调(中间超声波)
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) else {
uint32_t fall_time = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1);
distances[0] = (fall_time - rise_time[0]) * 0.017f; // 计算距离(cm)
}
}
}
// TIM12中断回调(左右超声波)
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) else {
uint32_t fall_time = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1);
distances[1] = (fall_time - rise_time[1]) * 0.017f;
}
__HAL_TIM_CLEAR_FLAG(htim, TIM_FLAG_CC1);
}
// 左边超声波(PB15)
if (__HAL_TIM_GET_FLAG(htim, TIM_FLAG_CC2)) else {
uint32_t fall_time = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_2);
distances[2] = (fall_time - rise_time[2]) * 0.017f;
}
__HAL_TIM_CLEAR_FLAG(htim, TIM_FLAG_CC2);
}
}
}
```
#### 非阻塞触发函数
```c
void trigger_ultrasonics(void)
current = (current + 1) % 3; // 循环触发
last_trigger = HAL_GetTick();
}
}
// 微秒级延时(非阻塞替代方案)
void delay_us(uint32_t us) {
uint32_t start = DWT->CYCCNT;
uint32_t cycles = us * (SystemCoreClock / 1000000);
while ((DWT->CYCCNT - start) < cycles);
}
```
#### GPIO初始化
```c
void GPIO_Init(void) {
// 启用时钟
__HAL_RCC_GPIOD_CLK_ENABLE();
__HAL_RCC_GPIOB_CLK_ENABLE();
// Trig引脚配置(推挽输出)
GPIO_InitTypeDef GPIO_InitStruct = {
.Pin = US_MID_TRIG_PIN | US_LEFT_TRIG_PIN,
.Mode = GPIO_MODE_OUTPUT_PP,
.Pull = GPIO_NOPULL,
.Speed = GPIO_SPEED_FREQ_HIGH
};
HAL_GPIO_Init(GPIOD, &GPIO_InitStruct);
GPIO_InitStruct.Pin = US_RIGHT_TRIG_PIN;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
// Echo引脚配置(输入)
GPIO_InitStruct.Pin = US_MID_ECHO_PIN;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Alternate = GPIO_AF2_TIM2; // PD6复用为TIM2_CH1
HAL_GPIO_Init(GPIOD, &GPIO_InitStruct);
GPIO_InitStruct.Pin = US_RIGHT_ECHO_PIN;
GPIO_InitStruct.Alternate = GPIO_AF9_TIM12; // PB14复用为TIM12_CH1
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
GPIO_InitStruct.Pin = US_LEFT_ECHO_PIN;
GPIO_InitStruct.Alternate = GPIO_AF9_TIM12; // PB15复用为TIM12_CH2
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
}
```
#### 主函数逻辑
```c
int main(void) {
HAL_Init();
SystemClock_Config();
// 启用DWT计数器(用于精确延时)
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
DWT->CYCCNT = 0;
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
GPIO_Init();
MX_TIM2_Init();
MX_TIM12_Init();
while (1) {
trigger_ultrasonics(); // 非阻塞触发
// 示例:获取距离数据(实际使用时可通过串口输出等)
float mid_dist = distances[0];
float right_dist = distances[1];
float left_dist = distances[2];
HAL_Delay(10); // 主循环延时(非测量阻塞)
}
}
```
#### 关键配置说明
1. **引脚复用**:
- PD6复用为TIM2_CH1(中间Echo)
- PB14复用为TIM12_CH1(右边Echo)
- PB15复用为TIM12_CH2(左边Echo)
2. **定时器配置**:
- TIM2(32位定时器):用于中间超声波
- TIM12(16位定时器):用于左右超声波
- 计数频率1MHz(1计数=1μs)
3. **非阻塞设计**:
- 分时触发(20ms周期)
- DWT计数器实现精确μs延时
- 双边沿捕获中断处理
4. **距离计算**:
$$ ext{距离}( ext{cm}) = frac{ ext{高电平时间}(mu ext{s}) imes 0.034}{2} approx ext{时间} imes 0.017 $$
> **校准提示**:实际声速受温度影响,可添加温度补偿:
> $$ ext{声速} = 331.3 imes sqrt{1 + frac{T}{273.15}} quad ( ext{m/s}) $$
> 其中$T$为摄氏温度[^1]。