• 正文
  • 相关推荐
申请入驻 产业图谱

光耦 + STM32 + MCP4725:把频率变成电压的一整套方案

05/29 13:41
467
加入交流群
扫码加入
获取工程师必备礼包
参与热点资讯讨论

车上有些传感器,输出的不是电压,不是电流,也不是 CAN 报文——而是一串方波。例如频率在50Hz到150Hz 之间蹦跶,占空比跟你没关系,只看频率。但下游那个仪表或者ECU,它只认模拟电压,0.5V到4.5V,线性的。中间这个"翻译"的活,就是本文要干的事。

在这篇文章中,我们将要使用STM32来精准统计一个12V方波输入的传感器将其转化为线性0.5V~4.5V对应的模拟电压值。

1、硬件设计

在写一行代码之前,先把硬件链路搭清楚。这玩意说简单也简单——三块东西串起来:输入级负责把12V方波安全地请进单片机,中间STM32负责干活,输出级负责把算好的结果变成模拟电压送出去。下面按信号流的方向,一块一块拆开讲。

第一个问题,12V 方波,能直接往 STM32 引脚上怼吗?

不能。不是耐压的问题,分压电阻怼两个也能把 12V 降到 3.3V。真正要命的是地。车上的电环境有多脏,玩过车载电子的人都知道——点火线圈发电机、各种继电器噼里啪啦地切,地回路上全是噪声。你把传感器的信号地和 STM32 的地直接连在一起,运气好读数乱跳,运气不好单片机直接复位,再差一点整块板子带走。

所以输入级必须隔离。光耦是最省事的方案,一颗芯片,电-光-电,输入端和输出端在物理上隔开的,两边各自用自己的地和电源,噪声传不过来。

我用的光耦是 EL357N。这玩意是晶体管输出的普通光耦,截止频率不算高,但对于 150Hz 以内的方波绰绰有余——150Hz 的周期是 6.67ms,EL357N 的上升/下降沿在几微秒级别,边沿占比不到千分之一,捕获精度完全够用。选它而不是高速光耦的原因也很简单——够用就行,一颗 EL357N 的价格便宜,而且手边正好有。

电路接法:输入端,12V方波串一个1kΩ左右的限流电阻进光耦的阳极,阴极接传感器的地。EL357N的输入正向电流推荐在 5mA 到 20mA 之间,12V供电,(12V - 1.2V) / 1kΩ = 10.8mA,刚好在推荐范围内。输出端,集电极接 3.3V 的上拉电阻(4.7kΩ),发射极接地,集电极出来的就是翻转后的 3.3V 方波,送到 STM32 的 PA11。

注意 EL357N 是反相输出的——输入是高电平时 LED 亮,输出端晶体管导通,集电极被拉到地,输出低电平。所以方波经过光耦之后相位反了,但在测频率这件事上完全不碍事,我们只关心两个相邻上升沿的时间间隔,相位爱反不反。

一句话总结:12V 方波推光耦 → EL357N 隔离输出 3.3V 方波 → PA11捕获。

同时为了兼容其他的电平协议,这里也设计了一个滑动开关来切换12V电平和5V电平(5V是能够支持3.3V驱动的,因为3.3V的高电平不足以驱动光耦)。

频率算完了,怎么把它变成模拟电压送出去?第一个反应可能是——STM32 不是有 DAC 吗,直接引脚输出不就完了?

STM32F103C8T6:不好意思,我没有。

这玩意就是这么现实。STM32F103 系列不是全线带 DAC 的,C8T6这颗料属于中密度器件,64K Flash,20K RAM,定时器管够,ADC有俩,但DAC——一个都没有。

那替代方案就两条路:PWM 加RC低通滤波模拟DAC,或者外挂一颗专用DAC芯片。

PWM + RC的方案便宜,搭两个电阻电容就完事。但问题也很明显——不仅仅需要加运放放大3.3V滤波后的电压,还需要精准的计算参数。

所以直接上MCP4725。12位分辨率,I2C通信,两根线往 PB6/PB7 上一挂就完事。最舒服的是供电范围——2.7V到5.5V,给它5V供电,输出就是0到5V,项目要求的0.5V到4.5V刚好在范围内,不用运放,不用 RC,直接出。

接线也非常省事。VCC接 5V,GND接地,SDA/SCL接 PB7/PB6。A0脚接地I2C地址是0x60,接VCC是 0x62,总线上只有一颗的话默认接地。OUT脚对地并一个100nF 去耦电容,压一下高频毛刺。

另外电源选用LM2596这块经典的5V DCDC稳压电源以及STM32F103C8T6系统部分就不在过多阐述了。

2、软件部分

PA11在F103C8T6上对应的是TIM1_CH4。TIM1是高级定时器,挂在APB2上,时钟72MHz。输入捕获的套路很固定:配好定时器和通道,设成上升沿捕获,中断里每抓到一次就把相邻两次时间戳一减,周期就有了,频率就是倒数。

定时器这边,预分频器填 71(72 分频,72MHz / 72 = 1MHz,1μs 分辨率),自动重装载调到最大 65535,向上计数。50Hz 的周期是 20000μs,150Hz 是 6666μs,都在 65535 以内,不会溢出。

通道 4 配成输入捕获模式,上升沿触发,IC4 映射到 TI4(直接映射不用交叉),不分频。

voidHAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim){  if (htim->Instance == TIM1)  {    //pre = 7199    tim1_count = __HAL_TIM_GET_COUNTER(htim);    Freuency = 72000000.0 / (tim1_count * 7200.0);    last_capture_tick = HAL_GetTick();    //清空计数器    __HAL_TIM_SET_COUNTER(htim, 0);  }}

然后就是中断服务函数。TIM1 的 CC4 中断里做三件事:读 CCR4 的值(当前捕获到的计数值),跟上一次捕获值做个差值取绝对值得到周期(单位 μs),更新上一次捕获值为当前值,最后清中断标志。周期的倒数乘以1000000就是频率。为了稳,采 8 次取一次平均丢给主循环用,别在中断里直接算频率——中断里干的活越少越好,拿完时间戳就走。

MCP4725的驱动比较复杂,这里应该会单开一个系列去讲,这里就直接上应用代码:

//启动TIM1  HAL_TIM_Base_Start_IT(&htim1);  //开启输入捕获中断  HAL_TIM_IC_Start_IT(&htim1, TIM_CHANNEL_4);  i2c_device_count = I2C_Scan(devices, 128);  dac = MCP4725_init(&hi2c1, MCP4725A0_ADDR_A00, MCP4725_REF_VOLTAGE);  mcp4725_connected = MCP4725_isConnected(&dac);  //初始化MCP4725,默认输出安全电压0V  mcp4725_last_write_ok = MCP4725_setVoltage(&dac, VOLT_SAFE, MCP4725_FAST_MODE, MCP4725_POWER_DOWN_OFF);
  while (1)  {    /* USER CODE END WHILE */    /* USER CODE BEGIN 3 */    float freq = Freuency;    uint32_t tick = last_capture_tick;    uint32_t now = HAL_GetTick();    float voltage;    uint8_t freq_fault;    freq_fault = ((now - tick) > SIGNAL_TIMEOUT_MS) || (freq < FREQ_SAFE_LIMIT);    if (freq_fault)    {        voltage = VOLT_SAFE;    }    else    {        if (freq < FREQ_MIN)            freq = FREQ_MIN;        if (freq > FREQ_MAX)            freq = FREQ_MAX;        voltage = FreqToVoltage(freq);    }    mcp4725_last_write_ok = MCP4725_setVoltage(&dac, voltage, MCP4725_FAST_MODE, MCP4725_POWER_DOWN_OFF);    if (freq_fault)    {      SetLedState(GPIO_PIN_SET, GPIO_PIN_RESET);    }    elseif (!mcp4725_last_write_ok)    {      if (((now / DAC_ERROR_BLINK_MS) & 1U) == 0U)      {        SetLedState(GPIO_PIN_SET, GPIO_PIN_RESET);      }      else      {        SetLedState(GPIO_PIN_SET, GPIO_PIN_SET);      }    }    else    {      SetLedState(GPIO_PIN_RESET, GPIO_PIN_SET);    }    HAL_Delay(10);  }  /* USER CODE END 3 */}

到了主循环,代码逻辑就不只是"读频率 → 算电压 → 写 DAC"这么简单了。实际跑在车上,你要考虑信号丢了怎么办、频率异常怎么办、DAC 写失败了怎么让外面的人知道。来看逐段拆解

先看故障判断。这里做了两层保护:

第一层,信号超时。每次输入捕获中断触发时更新一个时间戳 last_capture_tick,主循环里拿当前系统毫秒 tick 跟它做差,超过 SIGNAL_TIMEOUT_MS 就认为方波没了——传感器掉了、线断了、光耦挂了,不管什么原因,反正没信号进来。第二层,频率踩底线。FREQ_SAFE_LIMIT 设一个安全下限,比如 10Hz,频率比这个还低,说明要么输入已经不可信,要么车子压根没在转。

任意一个条件触发,就是 freq_fault,直接输出安全电压 VOLT_SAFE。这个安全电压设多少取决于你的下游设备——比如设成 0.5V 让仪表归零,或者设成 2.5V 让 ECU 知道信号状态异常。别在故障时还傻傻地输出最后一个有效值,下游设备拿着一个假数值继续做事,后果可能比没信号更严重。

正常路径反而简单了。频率限幅到 FREQ_MIN ~ FREQ_MAX 之间,然后丢进 FreqToVoltage 算电压,写进 MCP4725。

效果展示

最后经过测试,50HZ~150HZ的频率被线性映射到0.5V~4.5V,万用表测试为0.49V到4.49V.同时有低频率(设备异常和无设备)警报

相关推荐