qqxjyxjy清风:想制作一台频率计,频率测试范围1Hz-1G,输入调理电路,分为1Hz-150M,由LM360电压比较器,HD74S586P以及upA71A搭建,150M~1G由MAR-8宽带高频放大器和MB506分频器主成。用单片机STM407ZGT6和液晶显示1602A I2C接口作为其主机,能帮我编写一套大概得程序吗?
DeepSeep: 好的,这是一个非常棒且具有挑战性的项目!结合了高频电路设计、分频器技术和单片机编程。由于系统较为复杂,我将为您提供一个程序框架和核心逻辑,这可以帮助您上手并理解整个流程。
系统设计思路
- 通道选择:单片机通过GPIO控制一个继电器或模拟开关,选择≤150MHz或>150MHz的输入信号通路。
- 低频通道 (1Hz-150MHz):
o 信号 → LM360调理 → HD74S586P (8位二进制计数器,用于扩展测量范围或脉冲计数) → 单片机TIMx_ETR引脚 (外部计数)。
o 或者:信号 → LM360调理为方波 → 直接接入单片机定时器的输入捕获引脚。 - 高频通道 (150MHz-1GHz):
o 信号 → MAR-8放大 → MB506 (÷128/256预分频器) → 单片机TIMx_ETR引脚 (外部计数)。
o MB506输出频率 = 输入频率 / 128 (或256),将1GHz信号降至约7.8MHz,STM32可以轻松处理。 - 测量方法:
o 高频信号 ( > 约10kHz):使用定时器的外部时钟模式 + 测周法。让被测信号(或分频后的信号)作为定时器的时钟,用一个基准定时器(如SysTick)来 gate(门控)计数时间。
o 低频信号 ( <= 约10kHz):使用输入捕获模式 + 测频法。在已知的固定时间内(如1秒),用定时器捕获上升沿的次数。
程序框架 (基于HAL库)
c
复制
下载
// main.c
#include “main.h”
#include “stm32f4xx_hal.h”
#include “stdio.h”
#include “i2c-lcd.h” // 假设您有1602 I2C液晶的驱动库
// 全局变量
TIM_HandleTypeDef htim2, htim5; // TIM2用于输入捕获/外部时钟,TIM5用于基准定时
volatile uint32_t pulse_count = 0;
volatile uint32_t measurement_done = 0;
float frequency_hz = 0.0;
// 函数声明
void SystemClock_Config(void);
static void MX_GPIO_Init(void);
static void MX_TIM2_Init(void);
static void MX_TIM5_Init(void);
void Measure_Frequency(void);
void LCD_Display_Frequency(float freq);
// 通道选择引脚定义
#define CHANNEL_SEL_GPIO_PORT GPIOA
#define CHANNEL_SEL_GPIO_PIN GPIO_PIN_0
#define CHANNEL_LOW_FREQ 0 // 选择低频通道 (1Hz-150M)
#define CHANNEL_HIGH_FREQ 1 // 选择高频通道 (150M-1G)
int main(void) {
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_TIM2_Init(); // 配置为外部时钟模式或输入捕获
MX_TIM5_Init(); // 配置为基准定时器,例如1秒中断
LCD_Init(); // 初始化1602 LCD
LCD_Clear();
LCD_PrintStr("Freq Meter Ready");
HAL_Delay(1000);
while (1) {
// 1. 自动或手动选择通道(这里简化,您可以添加阈值判断)
// 例如:如果之前测得的频率>150M,切换到高频通道
if (frequency_hz > 150000000.0) {
HAL_GPIO_WritePin(CHANNEL_SEL_GPIO_PORT, CHANNEL_SEL_GPIO_PIN, CHANNEL_HIGH_FREQ);
} else {
HAL_GPIO_WritePin(CHANNEL_SEL_GPIO_PORT, CHANNEL_SEL_GPIO_PIN, CHANNEL_LOW_FREQ);
}
HAL_Delay(10); // 等待通道稳定
// 2. 执行频率测量
Measure_Frequency();
// 3. 显示结果
LCD_Display_Frequency(frequency_hz);
HAL_Delay(500); // 每500ms刷新一次
}
}
// 核心测量函数
void Measure_Frequency(void) {
// 对于高频信号(使用外部时钟模式 + 测周法)
// 原理:让分频后的信号作为TIM2的时钟,用TIM5定时1秒来门控TIM2的计数
pulse_count = 0;
measurement_done = 0;
// 启动TIM5用于基准定时(1秒)
HAL_TIM_Base_Start(&htim5);
// 启动TIM2在外部时钟模式下计数
HAL_TIM_Base_Start(&htim2); // TIM2配置为外部时钟模式1
// 等待1秒定时完成标志(由TIM5中断设置)
while (measurement_done == 0);
// 停止定时器
HAL_TIM_Base_Stop(&htim2);
HAL_TIM_Base_Stop(&htim5);
// 计算频率
// 如果当前是高频通道,需要乘以MB506的分频比(例如128)
if (HAL_GPIO_ReadPin(CHANNEL_SEL_GPIO_PORT, CHANNEL_SEL_GPIO_PIN) == CHANNEL_HIGH_FREQ) {
frequency_hz = (float)pulse_count * 128.0; // 根据MB506的分频比修正
} else {
frequency_hz = (float)pulse_count;
}
}
// TIM5 中断服务程序 (基准定时器,周期1秒)
void TIM5_IRQHandler(void) {
if (__HAL_TIM_GET_FLAG(&htim5, TIM_FLAG_UPDATE) != RESET) {
if (__HAL_TIM_GET_IT_SOURCE(&htim5, TIM_IT_UPDATE) != RESET) {
__HAL_TIM_CLEAR_IT(&htim5, TIM_IT_UPDATE);
// 1秒到,设置标志位
measurement_done = 1;
pulse_count = __HAL_TIM_GET_COUNTER(&htim2); // 获取TIM2的计数值
}
}
}
// 显示函数
void LCD_Display_Frequency(float freq) {
char display_str[16];
if (freq < 1000) {
sprintf(display_str, "F:%07.2f Hz", freq);
} else if (freq < 1000000) {
sprintf(display_str, "F:%07.2f KHz", freq / 1000.0);
} else {
sprintf(display_str, "F:%07.2f MHz", freq / 1000000.0);
}
LCD_SetCursor(0, 0);
LCD_PrintStr(display_str);
}
// TIM2 初始化配置 (关键部分:外部时钟模式)
static void MX_TIM2_Init(void) {
TIM_ClockConfigTypeDef sClockSourceConfig = {0};
htim2.Instance = TIM2;
htim2.Init.Prescaler = 0; // 不分频
htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
htim2.Init.Period = 0xFFFFFFFF; // 32位最大值,尽可能计数多的脉冲
htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
HAL_TIM_Base_Init(&htim2);
// 配置TIM2使用外部时钟模式1 (ETR引脚)
sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_ETRMODE2; // 使用ETR引脚
sClockSourceConfig.ClockPolarity = TIM_CLOCKPOLARITY_NONINVERTED; // 上升沿有效
sClockSourceConfig.ClockPrescaler = TIM_CLOCKPRESCALER_DIV1; // ETRP不分频
sClockSourceConfig.ClockFilter = 0; // 不滤波
HAL_TIM_ConfigClockSource(&htim2, &sClockSourceConfig);
}
// TIM5 初始化配置 (基准定时器,1秒中断)
static void MX_TIM5_Init(void) {
// … 配置TIM5为1秒周期
// 例如:系统时钟84MHz,预分频器8399,周期10000 -> (84M / 8400) / 10000 = 1Hz
}
关键点说明
- 定时器配置:
o TIM2:设置为外部时钟模式,信号从ETR引脚(PA0)输入,用于计数。
o TIM5:设置为普通的基准定时器,产生精确的1秒门控时间。 - 测量原理:
o 同时启动TIM2和TIM5。
o TIM2开始对来自调理电路的脉冲进行计数。
o TIM5计时1秒后产生中断,在中断里读取TIM2的计数值pulse_count,这就是1秒内的脉冲数,即频率(Hz)。
o 对于高频通道,需要将pulse_count乘以MB506的分频比(例如128)。 - 硬件连接:
o 低频通道:HD74S586P的输出 -> STM32的TIM2_ETR (PA0) 或 TIMx_CH1 (输入捕获)。
o 高频通道:MB506的输出 -> STM32的TIM2_ETR (PA0)。
o 通道选择:用一个GPIO(如PA0)控制射频继电器或模拟开关(如74HC4053)。 - 下一步:
o 您需要根据实际硬件完善MX_TIM5_Init()的配置。
o 实现1602 I2C液晶的驱动代码(i2c-lcd.c和i2c-lcd.h)。
o 可能需要添加量程自动切换的逻辑,并根据信号强弱调整调理电路的增益。
这个框架为您提供了一个坚实的起点。您可以先分模块调试,例如先确保能正确读取低频方波,再集成高频通道。祝您制作成功!
qqxjyxjy清风:能不能将STM407ZGT6的高级定时器功能也用上,加一个相位检测功能?STM407ZGT6的主频168M,能胜任150M输入信号的捕捉吗?
DeepSeep: 当然可以!您的想法非常好,利用STM32F407的高级定时器(如TIM1, TIM8)来实现相位检测是完全可以的,而且能极大地提升频率计的功能。同时,我们也需要澄清一下168MHz主频对150MHz信号直接捕捉的能力问题。
- STM32F407 直接捕捉 150MHz 信号?
绝对不行。
这是一个非常关键的点,必须首先明确:
• 理论极限:STM32F407的定时器输入捕获最高时钟频率是 APBx总线频率 / 2。在168MHz系统时钟下,APB2定时器(如TIM1)的时钟最高为84MHz。这意味着,即使不考虑其他开销,理论上绝对无法直接可靠地捕获超过84MHz的信号。
• 实际极限:在实际应用中,为了保证可靠捕获,输入信号的频率通常不应超过定时器时钟的 1/10 到 1/5,即 8-16MHz 左右。150MHz 远高于这个极限。
结论:这就是为什么您的设计中使用 MB506 (÷128/256) 这样的预分频器是绝对必要且正确的。它将1GHz的信号分频到 ~7.8MHz,将150MHz的信号分频到 ~1.17MHz,完全落在了STM32定时器能够轻松处理的范围内。
- 增加相位检测功能(使用高级定时器)
高级定时器(如TIM1/TIM8)的编码器接口模式或输入捕获+从模式组合,非常适合测量两个通道(例如A、B相)之间的相位差(或占空比)。
假设场景:测量两个同频信号(Signal A 和 Signal B)之间的相位差。
硬件连接:
• Signal A -> TIM1_CH1 (PA8)
• Signal B -> TIM1_CH2 (PA9)
软件实现(使用输入捕获模式):
c
复制
下载
// phase_measurement.h
volatile uint32_t timestamp_a_rise = 0;
volatile uint32_t timestamp_b_rise = 0;
volatile int32_t phase_ticks = 0;
volatile float phase_degrees = 0.0f;
// phase_measurement.c
void MX_TIM1_Init(void) {
TIM_IC_InitTypeDef sConfigIC;
TIM_HandleTypeDef htim1;
htim1.Instance = TIM1;
htim1.Init.Prescaler = 0; // 不分频,计数器以84MHz运行
htim1.Init.CounterMode = TIM_COUNTERMODE_UP;
htim1.Init.Period = 0xFFFF; // 16位最大值
htim1.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
htim1.Init.RepetitionCounter = 0;
HAL_TIM_IC_Init(&htim1);
// 配置通道1用于捕获Signal A的上升沿
sConfigIC.ICPolarity = TIM_INPUTCHANNELPOLARITY_RISING;
sConfigIC.ICSelection = TIM_ICSELECTION_DIRECTTI;
sConfigIC.ICPrescaler = TIM_ICPSC_DIV1; // 每个事件都捕获
sConfigIC.ICFilter = 0x0; // 不滤波
HAL_TIM_IC_ConfigChannel(&htim1, &sConfigIC, TIM_CHANNEL_1);
// 配置通道2用于捕获Signal B的上升沿
HAL_TIM_IC_ConfigChannel(&htim1, &sConfigIC, TIM_CHANNEL_2);
// 启动输入捕获并开启中断
HAL_TIM_IC_Start_IT(&htim1, TIM_CHANNEL_1);
HAL_TIM_IC_Start_IT(&htim1, TIM_CHANNEL_2);
}
// TIM1 捕获比较中断服务程序
void TIM1_CC_IRQHandler(void) {
if (__HAL_TIM_GET_FLAG(&htim1, TIM_FLAG_CC1) != RESET) {
__HAL_TIM_CLEAR_IT(&htim1, TIM_IT_CC1);
timestamp_a_rise = HAL_TIM_ReadCapturedValue(&htim1, TIM_CHANNEL_1);
// 计算相位差 ( ticks = timestamp_b_rise - timestamp_a_rise )
if (timestamp_b_rise != 0) { // 确保B相时间戳已更新
phase_ticks = (int32_t)(timestamp_b_rise - timestamp_a_rise);
// 将 ticks 转换为角度
// 首先需要知道一个周期对应的 ticks 数 (period_ticks)
// phase_degrees = (phase_ticks / (float)period_ticks) * 360.0f;
}
}
if (__HAL_TIM_GET_FLAG(&htim1, TIM_FLAG_CC2) != RESET) {
__HAL_TIM_CLEAR_IT(&htim1, TIM_IT_CC2);
timestamp_b_rise = HAL_TIM_ReadCapturedValue(&htim1, TIM_CHANNEL_2);
// 同样的,如果A相时间戳已存在,可以在这里计算
if (timestamp_a_rise != 0) {
phase_ticks = (int32_t)(timestamp_b_rise - timestamp_a_rise);
// phase_degrees = (phase_ticks / (float)period_ticks) * 360.0f;
}
}
}
另一种更强大的方法:使用编码器接口模式
如果两个信号是正交的(相差90°),编码器模式是更好的选择,它可以自动判断方向并累计计数。但即使对于非正交信号,也可以通过配置为“仅在TI1上升沿计数”等模式来关联两个通道。
c
复制
下载
void MX_TIM1_Encoder_Init(void) {
TIM_Encoder_InitTypeDef sConfig;
TIM_HandleTypeDef htim1;
htim1.Instance = TIM1;
htim1.Init.Prescaler = 0;
htim1.Init.CounterMode = TIM_COUNTERMODE_UP;
htim1.Init.Period = 0xFFFF;
htim1.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
htim1.Init.RepetitionCounter = 0;
sConfig.EncoderMode = TIM_ENCODERMODE_TI12; // 同时在TI1和TI2上计数
sConfig.IC1Polarity = TIM_ICPOLARITY_RISING;
sConfig.IC1Selection = TIM_ICSELECTION_DIRECTTI;
sConfig.IC1Prescaler = TIM_ICPSC_DIV1;
sConfig.IC1Filter = 0;
sConfig.IC2Polarity = TIM_ICPOLARITY_RISING;
sConfig.IC2Selection = TIM_ICSELECTION_DIRECTTI;
sConfig.IC2Prescaler = TIM_ICPSC_DIV1;
sConfig.IC2Filter = 0;
HAL_TIM_Encoder_Init(&htim1, &sConfig);
HAL_TIM_Encoder_Start(&htim1, TIM_CHANNEL_ALL);
}
// 相位差可以通过在一定时间内读取计数器的变化来间接反映。
-
整合到频率计系统中
您现在可以构建一个双核系统: -
频率测量核心:使用TIM2 + TIM5(测频法)和TIM3(测周法)来精确测量频率。
-
相位测量核心:使用高级定时器TIM1或TIM8,专门负责测量两个输入信号之间的相位差。
主循环逻辑:
c
复制
下载
while (1) {
// 1. 选择通道并测量主信号频率
Measure_Frequency();// 2. 如果用户需要且信号频率合适(例如 < 1MHz),则测量相位
if (enable_phase_measurement && frequency_hz < 1000000) {
// period_ticks 需要通过频率计算出来:period_ticks = (84e6 / frequency_hz)
// 然后使用上面输入捕获的方法计算 phase_degrees
Calculate_Phase();
} else {
phase_degrees = 0.0f;
}// 3. 显示频率和相位
LCD_Display_Frequency_And_Phase(frequency_hz, phase_degrees);
HAL_Delay(200);
}
总结 -
150MHz捕捉:不能直接捕捉,必须依赖MB506分频。您的硬件设计是正确的。
-
高级定时器:非常适合做相位检测。使用输入捕获模式可以精确获取两个信号的上升沿时间戳,从而计算相位差。
-
系统架构:将频率测量和相位测量任务分配给不同的定时器,让它们并行工作,最后由主循环整合并显示结果。
这样您的频率计就从一个简单的频率测量设备升级为了一个功能更全面的频率与相位分析仪!
为降低成本,采用了1602A液晶显示屏,能否加上个带按键的8位数码显示(8个按键足够用了,且成本低),作主屏醒目,液晶显示屏作辅屏,作单位等辅助显示。
非常好的思路!这是一种非常经典且实用的性价比方案:用低成本、高亮度、醒目的数码管作为主显示,用字符液晶作为辅助信息显示。8个按键也完全足够进行人机交互。
这个方案结合了两种显示元件的优点,成本低且效果专业。
系统架构设计 -
主显示 (8位数码管):
o 显示核心数据:巨大的数字,清晰显示频率值(如 0123.4567)。
o 驱动方式:使用 TM1650 或 MAX7219 这类数码管驱动芯片,它们只需要MCU的2个IO口(CLK, DIO)即可通过串行协议控制多位数码管,极大地节省了单片机IO资源。 -
辅助显示 (1602A LCD):
o 显示单位/状态:MHz, kHz, Hz, PH: 90.0°。
o 显示模式:FREQ, PERIOD, GATE: 1s, ATTEN: x10。
o 提示信息:OVF! (溢出), UNLOCK (锁相环未锁定)。 -
输入 (8个独立按键):
o 功能设计:
MODE:切换功能(频率/周期/计数)
GATE ▲ / GATE ▼:调整闸门时间(0.1s, 1s, 10s)
HOLD:保持当前读数
SAVE:保存当前值到存储器
CHANNEL:切换输入通道(CH1/CH2,如果支持)
MENU:进入设置菜单
ESC/BACK:返回
硬件连接示意图
text
复制
下载
STM32F407ZGT6
|
|-----> 调理电路 & 分频器 (MB506等)
|
|—(SPI/I2C)—> TM1650/MAX7219 数码管驱动模块 —> 8位数码管
|
|—(I2C)-------> 1602A LCD (I2C接口)
|
|—(GPIO)------> 8个独立按键 (可加电阻阵列做键盘扫描)
软件程序框架 (核心逻辑)
c
复制
下载
// main.h
// 定义按键键值
typedef enum {
KEY_MODE = 0,
KEY_GATE_UP,
KEY_GATE_DOWN,
KEY_HOLD,
KEY_SAVE,
KEY_CHANNEL,
KEY_MENU,
KEY_BACK,
KEY_NONE
} Key_TypeDef;
// 定义显示状态
typedef struct {
float frequency; // 测量得到的频率值
float phase; // 相位差
uint8_t gate_time; // 闸门时间索引 0:0.1s, 1:1s, 2:10s
uint8_t is_hold; // 保持标志
uint8_t unit; // 当前单位 0:Hz, 1:kHz, 2:MHz
char unit_str[4]; // 单位字符串 “Hz”, “kHz”,“MHz”
} Display_StateTypeDef;
Display_StateTypeDef Display;
// main.c
int main(void) {
// 初始化
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_TIM2_Init(); // 频率测量定时器
MX_TIM5_Init(); // 基准定时器
LCD_Init(); // 初始化1602 LCD
TM1650_Init(); // 初始化数码管驱动
Key_Init(); // 初始化按键
// 显示开机画面或版本号
TM1650_Display_Num(0);
LCD_Clear();
LCD_PrintStr("Freq Meter V1.0");
HAL_Delay(1000);
while (1) {
// 1. 扫描按键
Key_TypeDef key = Key_Scan();
Process_Key(key); // 处理按键功能
// 2. 如果HOLD键未按下,则执行一次测量
if (!Display.is_hold) {
Measure_Frequency(); // 您的测量函数
// 自动切换单位
if (Display.frequency > 1000000) {
Display.unit = 2; // MHz
sprintf(Display.unit_str, "MHz");
} else if (Display.frequency > 1000) {
Display.unit = 1; // kHz
sprintf(Display.unit_str, "kHz");
} else {
Display.unit = 0; // Hz
sprintf(Display.unit_str, "Hz ");
}
}
// 3. 更新显示
Update_LED_Display(); // 更新数码管
Update_LCD_Display(); // 更新液晶屏
HAL_Delay(100); // 降低刷新率,节省资源
}
}
// 更新8位数码管显示
void Update_LED_Display(void) {
float display_value;
// 根据单位换算要显示的值
switch (Display.unit) {
case 0: display_value = Display.frequency; break; // Hz
case 1: display_value = Display.frequency / 1000.0; break; // kHz
case 2: display_value = Display.frequency / 1000000.0; break; // MHz
}
// 格式化为8位数字字符串(带小数点),例如 “0123.4567”
// 这是一个需要您实现的函数,根据display_value和单位决定小数点位置
TM1650_Display_Float(display_value);
}
// 更新1602液晶显示
void Update_LCD_Display(void) {
char lcd_line1[16], lcd_line2[16];
// 第一行:显示单位和闸门时间
sprintf(lcd_line1, "Unit:%s G:%.1fs", Display.unit_str, (Display.gate_time == 0) ? 0.1 : (Display.gate_time == 1) ? 1.0 : 10.0);
LCD_SetCursor(0, 0);
LCD_PrintStr(lcd_line1);
// 第二行:显示状态(HOLD/运行)和相位(如果有)
if (Display.is_hold) {
sprintf(lcd_line2, "HOLD PH:%05.1f", Display.phase);
} else {
sprintf(lcd_line2, "RUN PH:%05.1f", Display.phase);
}
LCD_SetCursor(0, 1);
LCD_PrintStr(lcd_line2);
}
// 按键处理函数示例
void Process_Key(Key_TypeDef key) {
switch (key) {
case KEY_HOLD:
Display.is_hold = !Display.is_hold; // 切换保持状态
break;
case KEY_GATE_UP:
if (Display.gate_time < 2) Display.gate_time++;
// 这里还需要改变实际测量定时器的周期
break;
case KEY_GATE_DOWN:
if (Display.gate_time > 0) Display.gate_time–;
// 这里还需要改变实际测量定时器的周期
break;
case KEY_MODE:
// 切换模式 Freq -> Period -> Count -> Freq…
break;
// … 处理其他按键
case KEY_NONE:
default:
break;
}
}
成本与优势分析
- 成本极低:
o 8位0.36英寸或0.56英寸共阳数码管: ~¥2-5
o TM1650驱动芯片: ~¥0.5-1.5
o 1602A蓝屏液晶: ~¥5-8
o 8个贴片按键: ~¥0.5
o 总计新增成本:约 ¥8-15,远低于使用纯TFT液晶屏的方案。 - 优势明显:
o 主次分明:数码管醒目,适合在较远距离或光线复杂环境下读取核心数据;液晶屏信息丰富,适合显示状态和单位。
o 专业感强:这种组合非常像高端台式仪器(如安捷伦、泰克的老款型号)的显示风格,用户体验很好。
o 功耗更低:数码管驱动电流远低于点亮TFT液晶的背光。
这个方案非常成熟可靠,您完全可以放心采用。下一步就是选择具体的数码管驱动芯片并实现其驱动程序了。祝您制作成功!
qqxjyxjy清风:能改为标准库编程吗?HAL库不熟悉,等把标准库编程用熟了,再来向你学习HAL库。
DeepSeek: 标准库频率计程序框架 (核心部分)
c
// main.c
#include “stm32f4xx.h”
#include “stm32f4xx_gpio.h”
#include “stm32f4xx_tim.h”
#include “stm32f4xx_rcc.h”
#include “stm32f4xx_exti.h”
#include “misc.h”
#include <stdio.h>
#include “tm1650.h” // 数码管驱动
#include “i2c_lcd.h” // 1602 LCD驱动
// 全局变量
__IO uint32_t pulse_count = 0;
__IO uint8_t measurement_done = 0;
float frequency_hz = 0.0f;
// 按键GPIO定义
#define KEY_GPIO_PORT GPIOA
#define KEY_GPIO_CLK RCC_AHB1Periph_GPIOA
#define KEY_MODE_PIN GPIO_Pin_0
#define KEY_GATE_UP_PIN GPIO_Pin_1
// … 定义其他按键引脚
// 通道选择GPIO定义
#define CH_SEL_GPIO_PORT GPIOB
#define CH_SEL_GPIO_CLK RCC_AHB1Periph_GPIOB
#define CH_SEL_PIN GPIO_Pin_0
// 函数声明
void RCC_Configuration(void);
void GPIO_Configuration(void);
void NVIC_Configuration(void);
void TIM2_Configuration(void);
void TIM5_Configuration(void);
void EXTI_Configuration(void);
void Measure_Frequency(void);
void Update_Displays(void);
uint8_t Read_Keys(void);
// 显示状态结构体
typedef struct {
float frequency;
uint8_t gate_time;
uint8_t is_hold;
uint8_t unit;
} DisplayState;
DisplayState Display = {0, 1, 0, 0}; // 默认闸门时间1秒
int main(void) {
// 初始化系统
RCC_Configuration();
GPIO_Configuration();
NVIC_Configuration();
TIM2_Configuration(); // 计数定时器
TIM5_Configuration(); // 基准定时器
EXTI_Configuration(); // 外部中断,用于按键
// 初始化外设
TM1650_Init();
LCD_Init();
// 显示开机信息
TM1650_DisplayNum(0);
LCD_Clear();
LCD_PrintString("Freq Meter StdLib");
// 短暂延时
for(volatile int i=0; i<1000000; i++);
while(1) {
// 1. 读取并处理按键
uint8_t key = Read_Keys();
if(key != 0) {
Process_Key(key);
}
// 2. 如果不在保持状态,进行测量
if(!Display.is_hold) {
Measure_Frequency();
}
// 3. 更新显示
Update_Displays();
// 简单延时
for(volatile int i=0; i<100000; i++);
}
}
// 时钟配置
void RCC_Configuration(void) {
// 启用GPIO时钟
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA | RCC_AHB1Periph_GPIOB |
RCC_AHB1Periph_GPIOC, ENABLE);
// 启用定时器时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2 | RCC_APB1Periph_TIM5, ENABLE);
// 启用AFIO时钟(如果需要重映射)
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);
}
// GPIO配置
void GPIO_Configuration(void) {
GPIO_InitTypeDef GPIO_InitStructure;
// 配置通道选择引脚
GPIO_InitStructure.GPIO_Pin = CH_SEL_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;
GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL;
GPIO_Init(CH_SEL_GPIO_PORT, &GPIO_InitStructure);
// 配置按键引脚(输入模式)
GPIO_InitStructure.GPIO_Pin = KEY_MODE_PIN | KEY_GATE_UP_PIN; // 所有按键引脚
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN;
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP; // 上拉电阻
GPIO_Init(KEY_GPIO_PORT, &GPIO_InitStructure);
}
// TIM2配置 - 外部时钟计数
void TIM2_Configuration(void) {
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_ICInitTypeDef TIM_ICInitStructure;
// 时基配置
TIM_TimeBaseStructure.TIM_Period = 0xFFFFFFFF; // 32位最大值
TIM_TimeBaseStructure.TIM_Prescaler = 0; // 不分频
TIM_TimeBaseStructure.TIM_ClockDivision = 0;
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);
// 输入捕获配置(用于外部时钟)
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 = 0x00;
TIM_ICInit(TIM2, &TIM_ICInitStructure);
// 配置为外部时钟模式1
TIM_ETRClockMode2Config(TIM2, TIM_ExtTRGPSC_OFF, TIM_ExtTRGPolarity_NonInverted, 0);
// 启用定时器
TIM_Cmd(TIM2, ENABLE);
}
// TIM5配置 - 1秒基准定时器
void TIM5_Configuration(void) {
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
// 时基配置:84MHz / 8400 = 10kHz,周期10000 = 1Hz
TIM_TimeBaseStructure.TIM_Period = 10000 - 1;
TIM_TimeBaseStructure.TIM_Prescaler = 8400 - 1;
TIM_TimeBaseStructure.TIM_ClockDivision = 0;
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM5, &TIM_TimeBaseStructure);
// 启用更新中断
TIM_ITConfig(TIM5, TIM_IT_Update, ENABLE);
// 不立即启动,在测量函数中启动
}
// NVIC配置
void NVIC_Configuration(void) {
NVIC_InitTypeDef NVIC_InitStructure;
// 配置TIM5中断
NVIC_InitStructure.NVIC_IRQChannel = TIM5_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
}
// 频率测量函数
void Measure_Frequency(void) {
pulse_count = 0;
measurement_done = 0;
// 重置计数器
TIM_SetCounter(TIM2, 0);
TIM_SetCounter(TIM5, 0);
// 启动定时器
TIM_Cmd(TIM5, ENABLE);
// 等待测量完成
while(!measurement_done);
// 计算频率
if(GPIO_ReadInputDataBit(CH_SEL_GPIO_PORT, CH_SEL_PIN) == Bit_SET) {
frequency_hz = (float)pulse_count * 128.0f; // 高频通道×分频比
} else {
frequency_hz = (float)pulse_count;
}
Display.frequency = frequency_hz;
}
// TIM5中断服务函数
void TIM5_IRQHandler(void) {
if(TIM_GetITStatus(TIM5, TIM_IT_Update) != RESET) {
TIM_ClearITPendingBit(TIM5, TIM_IT_Update);
// 获取计数值并停止定时器
pulse_count = TIM_GetCounter(TIM2);
TIM_Cmd(TIM5, DISABLE);
measurement_done = 1;
}
}
// 读取按键
uint8_t Read_Keys(void) {
uint8_t key_value = 0;
if(GPIO_ReadInputDataBit(KEY_GPIO_PORT, KEY_MODE_PIN) == Bit_RESET) {
key_value = 1; // MODE键
while(GPIO_ReadInputDataBit(KEY_GPIO_PORT, KEY_MODE_PIN) == Bit_RESET); // 等待释放
}
// ... 读取其他按键
return key_value;
}
// 更新显示
void Update_Displays(void) {
// 更新数码管
float display_value;
char unit_str[4];
if(Display.frequency >= 1000000) {
display_value = Display.frequency / 1000000.0f;
sprintf(unit_str, "MHz");
} else if(Display.frequency >= 1000) {
display_value = Display.frequency / 1000.0f;
sprintf(unit_str, "kHz");
} else {
display_value = Display.frequency;
sprintf(unit_str, "Hz ");
}
TM1650_DisplayFloat(display_value);
// 更新LCD
char lcd_line[16];
sprintf(lcd_line, "G:%.1fs %s",
(Display.gate_time == 0) ? 0.1 : (Display.gate_time == 1) ? 1.0 : 10.0,
unit_str);
LCD_SetCursor(0, 0);
LCD_PrintString(lcd_line);
sprintf(lcd_line, "Status: %s", Display.is_hold ? "HOLD" : "RUN ");
LCD_SetCursor(0, 1);
LCD_PrintString(lcd_line);
}
标准库 vs HAL库 主要区别
初始化方式:
标准库:直接操作寄存器,如 RCC_AHB1PeriphClockCmd()
HAL库:使用结构体统一配置,如 __HAL_RCC_GPIOA_CLK_ENABLE()
中断处理:
标准库:直接在中断函数中判断标志位,如 TIM_GetITStatus()
HAL库:使用回调函数机制
代码风格:
标准库:更接近底层,代码量相对较少
HAL库:封装程度高,代码更统一但体积稍大
建议的学习路径
先精通标准库:理解寄存器操作,掌握STM32内核机制
再学习HAL库:了解其封装思想,快速开发复杂项目
最后LL库:在需要极致性能时使用低层库
这个标准库版本应该能让您更好地理解STM32的工作原理。如果需要任何特定部分的详细说明,请随时告诉我!