
本节目标看懂超声波测距时序和距离公式学会用 MCPWM 输入捕获测出微秒级脉宽并换算成距离一、本节要解决的问题避障、液位、距离检测都要求非接触测距。HC-SR04 模块便宜、接口简单两根 GPIO 就能用是入门最常用的方案。它的输出不是现成的距离数字而是一段高电平信号高电平持续多久声波就走了多久。要把时间变成距离需要解决两个问题读懂模块时序怎么触发测距、从哪个引脚读回时间测准微秒级的脉冲宽度用 GPIO 软件轮询误差很大本节改用MCPWM 的硬件输入捕获来测。二、硬件基础2.1 模块引脚引脚功能VCC电源教程写 3.3~5V数据手册参数表标 DC 5V实际以板子原理图为准Trig触发输入给一个 10µs 以上的高电平脉冲模块开始一次测距Echo回响输出测距期间输出高电平高电平持续时间 超声波往返时间GND地仓库代码里的接线//TRIG和ECHO对应的GPIO#defineHC_SR04_TRIG_GPIOGPIO_NUM_32#defineHC_SR04_ECHO_GPIOGPIO_NUM_33注意教程正文没有直接写出引脚号画在原理图里。仓库代码用的是Trig→GPIO32、Echo→GPIO33与开发板原理图上Trig R5 IO32、Echo R6 IO33的网络一致Echo 线上还串了 100Ω 电阻。实际接线以自己板子的原理图为准。2.2 工作时序一次测距分四步Trig 收到一个10µs 左右的高电平脉冲模块内部自动连续发射8 个 40kHz的超声波脉冲发射的同时 Echo 拉高声波遇到障碍物反射回来、被模块接收后 Echo 拉低Echo 高电平的持续时间就是发射→返回的往返时间 T。数据手册给出的主要参数参数值测距范围2cm ~ 400cm测距精度约 3mm测量角度15°触发信号10µs TTL 脉冲工作频率40kHz推荐测量周期60ms 以上三、知识点3.1 距离公式D v × T / 2设 Echo 高电平时间为 T声速 v ≈ 340m/s。因为 T 是往返时间单程距离要除以 2D v * T / 2代码里除以 58是这样推出来的D(cm) 340m/s * T(µs) / 2 34000cm/s * (10^-6)s/µs * T / 2 0.017 * T T / 58.8 ≈ T / 58注意340m/s 是常温约 15℃附近的声速温度每升高 1℃声速约增加 0.6m/s高精度场合需要温度补偿。另外 1/0.017 ≈ 58.8代码用 58 会让读数系统性偏大约 1.4%真实 100cm 会读成约 101.4cm。3.2 为什么必须用输入捕获如果这样用软件轮询while(!gpio_get_level(ECHO));// 记录时间while(gpio_get_level(ECHO));任务随时可能被 RTOS 调度打断、被其他中断抢占测出来的时间误差不可控。输入捕获的思路是让硬件来计时GPIO 检测到指定边沿上升沿/下降沿时硬件自动把当前定时器的计数值保存下来并触发中断中断里只需把两次计数值相减就得到脉宽对应的 ticks 数。软件只负责换算不负责掐表。3.3 ESP-IDF 里用 MCPWM 做捕获ESP-IDF 的MCPWM组件本来是用来驱动电机的 PWM 模块但自带捕获单元本工程只用捕获、不配置任何 PWM 输出。用到的对象有两个mcpwm_cap_timer_handle_t捕获定时器提供计数时基mcpwm_cap_channel_handle_t捕获通道绑定 GPIO 与捕获边沿并挂一个回调函数。捕获定时器使用默认时钟源内部经过分频的 APB 时钟prescale 1表示不分频。要把计数值换算成微秒需要知道时钟频率代码用esp_clk_apb_freq()获取一个 tick 的时长 1 / f(Hz) 秒 1000000 / f 微秒 脉宽(µs) tof_ticks * 1000000.0 / esp_clk_apb_freq()四、代码分析4.1 工程结构sr04/ ├── CMakeLists.txt ├── main/ │ ├── CMakeLists.txt # SRCS main.c │ └── main.c # 全部实现初始化 回调 触发 换算 └── sdkconfig # 目标芯片 esp32全部代码放在一个main.c里app_main()既做初始化又当主循环。4.2 初始化捕获定时器与通道//新建一个捕获定时器使用默认的时钟源mcpwm_cap_timer_handle_tcap_timerNULL;mcpwm_capture_timer_config_tcap_conf{.clk_srcMCPWM_CAPTURE_CLK_SRC_DEFAULT,.group_id0,};ESP_ERROR_CHECK(mcpwm_new_capture_timer(cap_conf,cap_timer));//新建一个捕获通道把捕获定时器与捕获通道绑定起来采取双边捕获的策略mcpwm_cap_channel_handle_tcap_chanNULL;mcpwm_capture_channel_config_tcap_ch_conf{.gpio_numHC_SR04_ECHO_GPIO,.prescale1,// capture on both edge.flags.neg_edgetrue,.flags.pos_edgetrue,// pull up internally.flags.pull_uptrue,};ESP_ERROR_CHECK(mcpwm_new_capture_channel(cap_timer,cap_ch_conf,cap_chan));上升沿、下降沿都捕获双边沿才能算出 Echo 的高电平宽度.prescale 1不分频时间分辨率最高.flags.pull_up true打开内部上拉避免 Echo 悬空时收到干扰。接着注册回调user_data传入当前任务句柄供中断里通知任务//新建一个捕获事件回调函数当捕获到相关的边沿则调用这个回调函数TaskHandle_t cur_taskxTaskGetCurrentTaskHandle();mcpwm_capture_event_callbacks_tcbs{.on_caphc_sr04_echo_callback,};ESP_ERROR_CHECK(mcpwm_capture_channel_register_event_callbacks(cap_chan,cbs,cur_task));//使能捕获通道ESP_ERROR_CHECK(mcpwm_capture_channel_enable(cap_chan));最后把 Trig 配成输出并拉低使能并启动捕获定时器//初始化Trig引脚设置为输出gpio_config_tio_conf{.modeGPIO_MODE_OUTPUT,.pin_bit_mask1ULLHC_SR04_TRIG_GPIO,};ESP_ERROR_CHECK(gpio_config(io_conf));ESP_ERROR_CHECK(gpio_set_level(HC_SR04_TRIG_GPIO,0));//使能捕获定时器ESP_ERROR_CHECK(mcpwm_capture_timer_enable(cap_timer));ESP_ERROR_CHECK(mcpwm_capture_timer_start(cap_timer));4.3 中断回调记录两个边沿的计数值staticboolhc_sr04_echo_callback(mcpwm_cap_channel_handle_tcap_chan,constmcpwm_capture_event_data_t*edata,void*user_data){staticuint32_tcap_val_begin_of_sample0;staticuint32_tcap_val_end_of_sample0;TaskHandle_t task_to_notify(TaskHandle_t)user_data;BaseType_t high_task_wakeuppdFALSE;//判断边沿上升沿记录捕获的计数值if(edata-cap_edgeMCPWM_CAP_EDGE_POS){// store the timestamp when pos edge is detectedcap_val_begin_of_sampleedata-cap_value;cap_val_end_of_samplecap_val_begin_of_sample;}else{//如果是下降沿也记录捕获的计数值cap_val_end_of_sampleedata-cap_value;//两个计数值的差值就是脉宽长度uint32_ttof_tickscap_val_end_of_sample-cap_val_begin_of_sample;// 通知任务计数差值xTaskNotifyFromISR(task_to_notify,tof_ticks,eSetValueWithOverwrite,high_task_wakeup);}returnhigh_task_wakeuppdTRUE;}上升沿只记录起点顺便把终点重置为起点避免混入上一轮的旧值下降沿计算差值用xTaskNotifyFromISR把结果发给主任务中断里只做最短的事换算和打印都交给任务返回值表示是否需要唤醒被通知的任务。4.4 主循环触发、等待、换算Trig 的高电平必须保持 10µs 左右vTaskDelay的最小粒度是毫秒级所以单独用一个忙等函数staticvoidgen_trig_output(void){gpio_set_level(HC_SR04_TRIG_GPIO,1);// set highesp_rom_delay_us(10);gpio_set_level(HC_SR04_TRIG_GPIO,0);// set low}主循环的核心uint32_ttof_ticks;while(1){//产生一个Triggen_trig_output();// 等待捕获完成信号if(xTaskNotifyWait(0x00,ULONG_MAX,tof_ticks,pdMS_TO_TICKS(1000))pdTRUE){floatpulse_width_ustof_ticks*(1000000.0/esp_clk_apb_freq());if(pulse_width_us35000){// 脉宽太长超出了SR04的计算范围continue;}floatdistance(float)pulse_width_us/58;ESP_LOGI(TAG,Measured distance: %.2fcm,distance);}vTaskDelay(pdMS_TO_TICKS(500));}xTaskNotifyWait最多等 1000ms正常测距结果很快到达超时说明没有回波本周期不打印下个周期重新触发程序不会卡死pulse_width_us 3500035ms约合 6m视为异常值既超出 HC-SR04 的 4m 量程也能挡掉上下沿错配导致的离谱结果每次测距后延时 500ms给模块留足恢复时间手册要求测量周期 60ms 以上。注意教程与仓库代码的差异教程代码清单从捕获定时器初始化开始没有列出gen_trig_output()和循环末尾的 500ms 延时仓库sr04/main/main.c里两者都存在。引用代码请以仓库为准。五、关键 API 速查API作用mcpwm_new_capture_timer()创建 MCPWM 捕获定时器时钟源、组号mcpwm_new_capture_channel()创建捕获通道绑定 GPIO、选择边沿、分频、上拉mcpwm_capture_channel_register_event_callbacks()注册捕获事件回调user_data可传任务句柄mcpwm_capture_channel_enable()使能捕获通道mcpwm_capture_timer_enable()/mcpwm_capture_timer_start()使能并启动捕获定时器gpio_config()/gpio_set_level()Trig 引脚配置为输出并输出触发脉冲esp_rom_delay_us()微秒级忙等延时产生 10µs TrigxTaskGetCurrentTaskHandle()获取当前任务句柄作为回调的 user_dataxTaskNotifyFromISR()/xTaskNotifyWait()中断给任务发通知 / 任务带超时等待通知esp_clk_apb_freq()读取 APB 时钟频率把计数值换算成微秒六、编译与现象验证cdesp32-board/sr04# 工程当前配置的目标就是 esp32idf.py build idf.py-pCOM端口flash monitor启动时依次打印Install capture timer、Install capture channel、Register capture callback、Enable capture channel、Configure Trig pin、Enable and start capture timer之后约每 0.5 秒打印一行I (xxxx) example: Measured distance: 25.43cm手掌在模块前前后移动数值随之变化完全挡住或超出量程的周期没有输出。