蓝桥杯单片机学习笔记(九)——超声波测距与NE555频率测量

温馨提示:内容源自米醋电子工作室培训课,由本人总结

一、超声波测距模块:单片机的"电子眼"

超声波测距就像蝙蝠的回声定位系统——发出声波,等待回声,通过时间差计算距离。在蓝桥杯竞赛中,官方资源包并未提供超声波底层驱动,需要我们自己编写。

1.1 硬件连接与引脚定义

查阅官方原理图后,定义超声波模块的两个关键引脚:

sbit Tx = P1^0;  // 发射端(Transmitter)
sbit Rx = P1^1;  // 接收端(Receiver)

引脚功能说明

  • Tx(发射端):发送超声波信号,像是"喇叭"
  • Rx(接收端):接收回波信号,像是"麦克风"

1.2 测距原理:声波的往返旅程

超声波测距的核心原理是时间差测距法,就像你在山谷中喊话,通过回声返回的时间判断距离。

1.2.1 物理公式推导

基本公式

距离 = 速度 × 时间 ÷ 2

为什么要除以2?

  • 超声波需要"去"和"回"两段路程
  • 测量的时间是往返总时间
  • 实际距离只是单程

代入声速计算

  • 声速:v = 340 m/s = 34000 cm/s
  • 距离公式:d = v × t ÷ 2
  • 简化结果:d = 34000 × t ÷ 2 = 17000 × t (cm)
  • 最终公式:d = 0.017 × t (cm)

其中t的单位是微秒(μs)

1.2.2 测距流程图解
[发射8个40kHz方波] → [等待回波] → [计算时间差] → [转换为距离]
       ↓                    ↓              ↓              ↓
    Tx引脚输出          Rx引脚检测      PCA定时器      d=0.017×t

1.3 超声波信号发射

1.3.1 为什么是40kHz?

超声波模块的谐振频率是40kHz,就像音叉有固定的振动频率,只有这个频率才能让模块高效工作。

40kHz方波参数计算

  • 频率:f = 40kHz = 40000Hz
  • 周期:T = 1/f = 1/40000 = 25μs
  • 半周期:T/2 = 12.5μs ≈ 12μs

方波时序图

Tx引脚电平:
    ___     ___     ___     ___
___|   |___|   |___|   |___|   |___
   12μs 12μs (重复8次)
1.3.2 延时函数生成

使用STC-ISP软件生成精确的12μs延时函数:

配置参数

  • 系统频率:12MHz
  • 指令集:Y1/STC89
  • 延时时间:12μs

生成代码

void Delay12us(void)  // @12.000MHz
{
    unsigned char data i;
    _nop_();
    i = 3;
    while (--i);
}

代码说明

  • _nop_():空操作指令,精确延时
  • i = 3:循环3次,配合空操作达到12μs
  • data关键字:将变量存储在data区,提高访问速度
1.3.3 超声波发射函数
// 超声波初始化函数:发射8个40kHz方波
void Ut_Wave_Init()
{
    unsigned char i;
    
    EA = 0;  // 关闭全局中断,防止时序被打断
    
    // 发射8个完整的方波周期
    for(i = 0; i < 8; i++)
    {
        Tx = 1;         // 拉高电平
        Delay12us();    // 保持12μs
        Tx = 0;         // 拉低电平
        Delay12us();    // 保持12μs
    }
    
    EA = 1;  // 恢复全局中断c
}

为什么发射8个方波?

  • 1个方波能量太弱,接收端可能检测不到
  • 8个方波形成"脉冲串",增强信号强度
  • 就像敲门要敲几下才能让人听见

为什么要关闭中断?

  • 40kHz方波时序要求极其精确
  • 中断会打断延时,导致频率偏移
  • 就像演奏乐器时不能被打断,否则节奏会乱

1.4 超声波接收与距离计算

1.4.1 为什么使用PCA定时器?

在蓝桥杯竞赛中,定时器资源非常紧张:

定时器占用情况

  • 定时器0:被NE555强制占用(频率测量)
  • 定时器1:串口通信占用(如果考到串口)
  • 定时器2:函数轮询和系统时基
  • PCA定时器:独立的可编程计数器阵列,可用于超声波

PCA定时器优势

  • 独立于传统定时器,不冲突
  • 16位计数范围,精度高
  • 可以测量脉冲宽度
1.4.2 PCA定时器寄存器说明
CMOD = 0x00;  // PCA计数器模式寄存器
              // 0x00:使用系统时钟/12作为计数时钟
              
CH = 0;       // PCA计数器高8位
CL = 0;       // PCA计数器低8位

CR = 1;       // PCA计数器运行控制位
              // 1:启动计数  0:停止计数

CF = 0;       // PCA计数器溢出标志
              // 1:计数溢出  0:未溢出

计数时钟计算

  • 系统时钟:12MHz
  • PCA时钟:12MHz ÷ 12 = 1MHz
  • 计数周期:1/1MHz = 1μs
  • 即:每计数1次 = 1μs
1.4.3 距离测量函数详解
// 超声波数据获取函数:返回测量距离(单位:cm)
unsigned char Ut_Wave_Data()
{
    unsigned int time;  // 存储往返时间(μs)
    
    // 步骤1:初始化PCA定时器
    CMOD = 0x00;        // 设置PCA工作模式
    CH = CL = 0;        // 清零计数器
    
    // 步骤2:发射超声波
    Ut_Wave_Init();
    
    // 步骤3:启动计时
    CR = 1;
    
    // 步骤4:等待回波或超时
    // Rx == 1:未接收到回波(接收端空闲为高电平)
    // CF == 0:计数器未溢出(未超时)
    while(Rx == 1 && CF == 0);
    
    // 步骤5:停止计时
    CR = 0;
    
    // 步骤6:判断是否成功接收
    if(CF == 0)  // 未溢出,说明接收到回波
    {
        // 读取计数值(往返时间)
        time = (CH << 8) | CL;  // 合并高低8位
        
        // 转换为距离:d = 0.017 × t
        return (time * 0.017);
    }
    else  // 溢出,说明超出测量范围
    {
        CF = 0;  // 清除溢出标志
        return 0;  // 返回0表示测量失败
    }
}

代码关键点解析

1. 时间合并操作

time = (CH << 8) | CL;
  • CH << 8:将高8位左移8位
  • | CL:与低8位进行或运算
  • 例如:CH=0x12, CL=0x34 → time=0x1234

2. 等待条件判断

while(Rx == 1 && CF == 0);
  • 两个条件同时满足才继续等待
  • 任一条件不满足就跳出循环
  • 就像等人:人没来且没超时就继续等

3. 距离计算

return (time * 0.017);
  • time单位是μs
  • 0.017是声速换算系数
  • 结果单位是cm
1.4.4 测量范围与精度

理论测量范围

  • PCA计数器:16位 = 65535
  • 最大时间:65535μs ≈ 65.5ms
  • 最大距离:65535 × 0.017 ≈ 1114cm ≈ 11m

实际测量范围

  • 有效范围:2cm ~ 400cm
  • 小于2cm:盲区,回波与发射重叠
  • 大于400cm:信号衰减严重,难以接收

测量精度

  • 时间精度:1μs
  • 距离精度:0.017cm ≈ 0.17mm
  • 实际精度:约±1cm(受环境影响)

1.5 完整驱动代码

1.5.1 ultrasound.h 头文件
#ifndef __ULTRASOUND_H__
#define __ULTRASOUND_H__

#include <STC15F2K60S2.H>

// 对外接口函数
unsigned char Ut_Wave_Data(void);  // 获取超声波测距数据(单位:cm)

#endif
1.5.2 ultrasound.c 源文件
/*  超声波测距驱动代码
    适用于蓝桥杯单片机竞赛
    测量范围:2cm ~ 400cm
    测量精度:±1cm
*/

#include "ultrasound.h"
#include <intrins.h>

// 引脚定义
sbit Tx = P1^0;  // 发射端
sbit Rx = P1^1;  // 接收端

// 12μs精确延时函数
void Delay12us(void)  // @12.000MHz
{
    unsigned char data i;
    _nop_();
    i = 3;
    while (--i);
}

// 超声波发射函数:发射8个40kHz方波
void Ut_Wave_Init(void)
{
    unsigned char i;
    
    EA = 0;  // 关闭全局中断
    
    for(i = 0; i < 8; i++)
    {
        Tx = 1;
        Delay12us();
        Tx = 0;
        Delay12us();
    }
    
    EA = 1;  // 恢复全局中断
}

// 超声波数据获取函数:返回测量距离(单位:cm)
unsigned char Ut_Wave_Data(void)
{
    unsigned int time;
    
    // 初始化PCA定时器
    CMOD = 0x00;
    CH = CL = 0;
    
    // 发射超声波
    Ut_Wave_Init();
    
    // 启动计时
    CR = 1;
    
    // 等待回波或超时
    while(Rx == 1 && CF == 0);
    
    // 停止计时
    CR = 0;
    
    // 判断是否接收到回波
    if(CF == 0)
    {
        time = (CH << 8) | CL;
        return (time * 0.017);
    }
    else
    {
        CF = 0;
        return 0;
    }
}

1.6 使用示例

#include "ultrasound.h"

void main(void)
{
    unsigned char distance;
    
    while(1)
    {
        // 获取距离数据
        distance = Ut_Wave_Data();
        
        // 判断测量是否有效
        if(distance > 0)
        {
            // 显示距离
            Display_Distance(distance);
        }
        else
        {
            // 显示错误信息
            Display_Error();
        }
        
        Delay_ms(100);  // 每100ms测量一次
    }
}

使用注意事项

  1. 测量频率不宜过高,建议间隔100ms以上
  2. 测量时避免障碍物倾斜角度过大
  3. 软质材料(如海绵)会吸收超声波,影响测量
  4. 测量距离为0时表示测量失败或超出范围

二、NE555频率测量:单片机的"频率计"

NE555是一款经典的定时器芯片,在蓝桥杯中用于产生可调频率的方波信号。我们需要用单片机测量这个频率,就像用频率计测量信号频率一样。

2.1 硬件连接与引脚冲突

2.1.1 引脚共用问题

重要提示:NE555的输出信号连接到P3.4引脚,这个引脚与独立按键S4共用!

引脚冲突示意图

P3.4 ─┬─ NE555信号输入(SIGNAL)
      │
      └─ 独立按键S4

解决方案

  • 如果题目考到NE555频率测量,必须屏蔽S4按键功能
  • 在按键扫描函数中,跳过P3.4的检测
  • 或者直接不使用S4按键
2.1.2 硬件跳线连接

关键操作:在开发板上,必须用跳线帽连接P34和SIGNAL两个引脚!

[P34] ←→ [SIGNAL]  (用跳线帽短接)

忘记连接的后果

  • 无法测量到频率
  • 测量值始终为0
  • 这是最常见的硬件错误

2.2 频率测量原理

2.2.1 测频原理:数脉冲

频率测量的本质是在固定时间内计数脉冲个数,就像数1秒钟内心跳了多少次。

测频公式

频率 = 脉冲个数 / 时间

实现方法

  • 使用定时器0的计数模式
  • 每来一个脉冲,计数器加1
  • 每隔1秒读取一次计数值
  • 读取后清零,重新计数
2.2.2 定时器0的两种模式

定时模式(Timer Mode)

  • 计数内部时钟脉冲
  • 用于产生精确延时
  • 我们之前一直在用

计数模式(Counter Mode)

  • 计数外部引脚(T0,即P3.4)的脉冲
  • 用于频率测量
  • NE555测量需要用这个模式

2.3 定时器0配置

2.3.1 TMOD寄存器配置
TMOD |= 0x05;

TMOD寄存器位定义

TMOD = 0x05 = 0000 0101
              ││││ ││││
              ││││ │││└─ M0 = 1 ┐
              ││││ ││└── M1 = 0 ┴─ 模式1(16位)
              ││││ │└─── C/T = 1 ─── 计数模式
              ││││ └──── GATE = 0 ── 不使用门控
              ││││
              │││└────── M0 = 0 ┐
              ││└─────── M1 = 0 ┴─ 定时器1配置
              │└──────── C/T = 0
              └───────── GATE = 0

关键位说明

  • C/T = 1:选择计数模式(Counter)
  • M1M0 = 01:16位计数器模式
  • GATE = 0:不使用门控功能
2.3.2 完整初始化代码

使用STC-ISP生成定时器1的1ms定时,然后修改定时器0为计数模式:

/* 定时器初始化函数 */
void Timer0_Init(void)  // 定时器0:计数模式
{
    AUXR &= 0x7F;      // 定时器时钟12T模式
    TMOD &= 0xF0;      // 清除定时器0配置位
    TMOD |= 0x05;      // 设置为计数模式,16位,不自动重装
    TL0 = 0x00;        // 计数器清零
    TH0 = 0x00;        // 计数器清零
    TF0 = 0;           // 清除溢出标志
    TR0 = 1;           // 启动定时器0
}

void Timer1_Init(void)  // 定时器1:1ms定时
{
    AUXR &= 0xBF;      // 定时器时钟12T模式
    TMOD &= 0x0F;      // 清除定时器1配置位
    TMOD |= 0x10;      // 设置为定时模式,16位
    TL1 = 0x18;        // 设置定时初值
    TH1 = 0xFC;        // 设置定时初值
    TF1 = 0;           // 清除TF1标志
    TR1 = 1;           // 定时器1开始计时
    ET1 = 1;           // 使能定时器1中断
    EA = 1;            // 开启总中断
}

初始化顺序要求

void main(void)
{
    Timer0_Init();  // 1. 先初始化定时器0(计数器)
    Timer1_Init();  // 2. 再初始化定时器1(时基)
    
    while(1)
    {
        // 主循环
    }
}

为什么要先初始化定时器0?

  • 定时器0需要先准备好接收脉冲
  • 定时器1启动后立即开始计时
  • 顺序错误可能导致第一次测量不准

2.4 频率读取与显示

2.4.1 全局变量定义
unsigned char Freq = 0;      // 存储测量到的频率值(单位:Hz)
unsigned int Time_1s = 0;    // 1秒计时器
bit Freq_Flag = 0;           // 频率有效标志位

变量说明

  • Freq:存储频率测量结果
  • Time_1s:累计定时器1中断次数,达到1000次=1秒
  • Freq_Flag:防止第一次测量误差的标志位
2.4.2 定时器1中断服务函数
/* 定时器1中断服务函数:每1ms执行一次 */
void Timer1_ISR(void) interrupt 3
{
    // 1秒计时
    if(++Time_1s == 1000)  // 1000ms = 1s
    {
        Time_1s = 0;  // 清零计时器
        
        // 读取定时器0的计数值(即频率)
        Freq = (TH0 << 8) | TL0;
        
        // 清零定时器0,准备下一次计数
        TH0 = 0;
        TL0 = 0;
        
        // 拉高频率有效标志
        Freq_Flag = 1;
    }
}

代码执行流程

每1ms中断一次 → Time_1s++ → 达到1000次?
                                 ↓ 是
                          读取TH0和TL0 → 存入Freq
                                 ↓
                          清零TH0和TL0 → 重新计数
                                 ↓
                          Freq_Flag = 1 → 数据有效
2.4.3 频率显示函数
/* 数码管显示函数 */
void Display_Frequency(void)
{
    // 只有标志位为1时才显示频率
    if(Freq_Flag == 1)
    {
        // 显示频率值
        Display_Number(Freq);
    }
    else
    {
        // 显示等待信息或0
        Display_Number(0);
    }
}

为什么需要Freq_Flag标志位?

问题分析

  • 定时器0在4T模式下,第一次测量可能不准确
  • 原因:定时器0启动时刻与定时器1不同步
  • 第一次测量的时间可能不足1秒

解决方案

  • 使用标志位Freq_Flag
  • 第一次1秒到达时,拉高标志位
  • 只有标志位为1时才显示频率
  • 这样确保显示的都是完整1秒的测量结果

时序示意图

定时器1启动 ─┬─ 0ms
             │
定时器0启动 ─┼─ 可能有延迟
             │
第一次1秒 ───┼─ 1000ms(数据可能不准)
             │   Freq_Flag = 1
             │
第二次1秒 ───┼─ 2000ms(数据准确)
             │   开始显示
             ↓

2.5 完整应用示例

#include <STC15F2K60S2.H>

// 全局变量
unsigned char Freq = 0;
unsigned int Time_1s = 0;
bit Freq_Flag = 0;

// 定时器初始化
void Timer0_Init(void)
{
    AUXR &= 0x7F;
    TMOD &= 0xF0;
    TMOD |= 0x05;  // 计数模式
    TL0 = 0x00;
    TH0 = 0x00;
    TF0 = 0;
    TR0 = 1;
}

void Timer1_Init(void)
{
    AUXR &= 0xBF;
    TMOD &= 0x0F;
    TMOD |= 0x10;  // 定时模式
    TL1 = 0x18;
    TH1 = 0xFC;
    TF1 = 0;
    TR1 = 1;
    ET1 = 1;
    EA = 1;
}

// 定时器1中断服务函数
void Timer1_ISR(void) interrupt 3
{
    if(++Time_1s == 1000)
    {
        Time_1s = 0;
        Freq = (TH0 << 8) | TL0;
        TH0 = TL0 = 0;
        Freq_Flag = 1;
    }
}

// 主函数
void main(void)
{
    // 初始化(顺序不能错)
    Timer0_Init();  // 先初始化计数器
    Timer1_Init();  // 再初始化时基
    
    while(1)
    {
        // 显示频率
        if(Freq_Flag)
        {
            Display_Number(Freq);
        }
        
        // 其他功能...
    }
}

2.6 常见问题与调试技巧

2.6.1 测量值始终为0

可能原因

  1. 忘记用跳线帽连接P34和SIGNAL
  2. 定时器0配置错误,未设置为计数模式
  3. NE555电路未工作

排查步骤

// 1. 检查硬件连接
// 确认跳线帽已连接

// 2. 检查TMOD配置
if((TMOD & 0x0F) == 0x05)
{
    // 配置正确
}

// 3. 检查定时器0是否运行
if(TR0 == 1)
{
    // 定时器0已启动
}
2.6.2 第一次测量不准确

原因分析

  • 定时器0和定时器1启动时刻不同步
  • 第一个1秒周期可能不完整

解决方法

  • 使用Freq_Flag标志位
  • 第一次测量后才开始显示
  • 代码示例见2.4.3节

2.7 测量范围与精度

理论测量范围

  • 定时器0:16位 = 65535
  • 最大频率:65535 Hz ≈ 65.5 kHz

实际测量范围

  • 有效范围:10 Hz ~ 10 kHz
  • 低于10Hz:测量时间过长,响应慢
  • 高于10kHz:可能超出计数范围

测量精度

  • 时间精度:±1ms(定时器1误差)
  • 频率精度:±1Hz
  • 相对误差:1/频率值
Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐