光guan是用本文章讲解一下:太阳能心率智能骑行仪的部分软硬件。
主控使用:STM32C8T6RC
下面是要求实现的功能:
制作一款能实现太阳能充电的智能骑行仪,完成行驶过程中的即时速度/平均速度显示,总里程数显示,时间显示,实时心率显示,夜间行车自动开灯等。
主要研究内容和工作如下:
(1)显示电量不足时,通过光照自动充电;有光线,即充电;
(2)显示屏上能显示即时速度,平均速度,总里程数;
(3)显示当前时间,断电后系统时间保持持续更新;
(4)增加照明功能,当行驶环境变暗后,自动开启照明灯;
(5)自行车把手增加心率检测电极,用于实时监测心率。当设定目标心率达到后进行声音报警;(可采用按键或实现蓝牙连接设置目标心率)
太阳能充电部分:
使用CN3791 MPPT模块,选择对应规格的太阳能板,以及对应的锂电池。
在检测到光照部分时候,有光照则CN3791 MPPT模块则能够通过LED充电指示灯,来显示是否进行了充电。在断电期间,则使用锂电池通过BAT和GND引脚输出对应电压。如下为模块图:
接口部分解释:SOLAR IN接入太阳能板,作者使用了6V的太阳能充电板,BAT引脚接入PH2.0接口的锂电池,之后通过OK指示灯进行充电的判断。BAT和GND引脚用来给其他外设和硬件电路进行供电。
速度测量:
使用3144霍尔传感器。模块图如下:
接口:VCC GND A0 D0 ,供电3.3-5V,A0引脚采集模拟电压,D0采集数字信号。
使用要在轮子上或者被测对象上,粘上一个磁铁,通过霍尔传感器在轮子转动一圈,则能够在磁铁部分收到一个脉冲信号,进而单片机内部进行计数。通过定时器进行卡好对应阈值,在一个阈值中检测到的脉冲数量。阈值/脉冲数量,则为轮子在对应时间内的转动次数。
转速计算公式: 𝑠𝑝𝑒𝑒𝑑 = (𝑡𝑖𝑚𝑒1 ∗ 2 ∗ 𝛱 ∗ 𝑑)/𝑇1
里程数计算公式: 𝑚𝑖𝑙𝑒𝑎𝑔𝑒 = 𝑡𝑖𝑚𝑒2 ∗ 2 ∗ 𝛱 ∗ d
其中 time1 为一个周期内检测到的车轮转动的圈数;d 为车轮的半径;time2 是一个周 期内检测到的车轮旋转的圈数;T1 为单片机定时器定时的时间;speed 为车轮旋转的瞬时 速度;mileage 为总共行驶的里程数;Tim_prescaler 为时钟预分频数;TIM_period 为时 钟的技术次数;systick 是主控的时钟频率,因为主晶振为 8Mhz 通过了倍频到 72Mhz,因此 systick 数值为 72Mhz。
代码部分:
外部中断初始化:3144传感器,接在了PB12口上。
void speed_Init(void)
{
EXTI_InitTypeDef EXTI_InitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE);
GPIO_EXTILineConfig(GPIO_PortSourceGPIOB,GPIO_PinSource12);
EXTI_InitStructure.EXTI_Line=EXTI_Line12;
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;
EXTI_Init(&EXTI_InitStructure);
NVIC_InitStructure.NVIC_IRQChannel = EXTI15_10_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x02; //抢占优先级2,
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x03; //子优先级3
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //使能外部中断通道
NVIC_Init(&NVIC_InitStructure);
}
定时器初始化:(设置阈值时间)
void TIM1_Int_Init(u16 arr,u16 psc)
{
NVIC_InitTypeDef NVIC_InitStructure;
GPIO_InitTypeDef GPIO_InitStructure;
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1,ENABLE);
//配置引脚
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);
//定时器TIM3初始化
TIM_TimeBaseStructure.TIM_Period = arr; //设置在下一个更新事件装入活动的自动重装载寄存器周期的值
TIM_TimeBaseStructure.TIM_Prescaler =psc; //设置用来作为TIMx时钟频率除数的预分频值
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; //设置时钟分割:TDTS = Tck_tim
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; //TIM向上计数模式
TIM_TimeBaseInit(TIM1, &TIM_TimeBaseStructure); //根据指定的参数初始化TIMx的时间基数单位
TIM_ClearFlag(TIM1,TIM_FLAG_Update);
TIM_ITConfig(TIM1,TIM_IT_Update,ENABLE ); //使能指定的TIM3中断,允许更新中断
//
NVIC_InitStructure.NVIC_IRQChannel = TIM1_UP_IRQn; //TIM3中断
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; //先占优先级0级
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 4; //从优先级3级
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ通道被使能
NVIC_Init(&NVIC_InitStructure); //初始化NVIC寄存器
TIM_Cmd(TIM1, ENABLE); //使能TIMx
}
定时器中断计算:
//定时器3中断服务程序
void TIM1_UP_IRQHandler(void) //TIM3中断
{
static u8 time3=0;
if (TIM_GetITStatus(TIM1, TIM_IT_Update) != RESET) //检查TIM3更新中断发生与否
{
TIM_ClearITPendingBit(TIM1, TIM_IT_Update); //清除TIMx更新中断标志
speed=3.6*time*(6.28*0.0325/1000);
mileage=3.6*time2*(6.28*0.0325/1000);
speed_val = mileage/time3;
time=0;
}
time3++;
}
//B15
void EXTI15_10_IRQHandler(void)
{
time++;
time2++;
EXTI_ClearITPendingBit(EXTI_Line12); //清除LINE0上的中断标志位
}
实时时钟:
使用DS3231模块,通过IIC协议进行数据的读写。同时内部集成了AT24C32作为内部的存储芯片。最大速度是400Khz。
使用该实时时钟需要考虑的是初始数据的设置,默认的实时时钟为系统的出厂设置, 因此需要在第一次烧录的时候要将设计初始时间,通过软件 IIC 写入 AT24C32 中,更新 系统时间,之后 DS3231 在此基础上通过内部计时来完成时间的记录,并且将其数据实 时更新到 AT24C32 中,通过查询该模块的芯片手册能够获得 DS3231 的数据写地址为 1101 0000,数据读地址为 1101 0001,因此通过 IIC 来对该地址进行读写操作,则能够实 现实时时钟的功能。
初次使用,写入一次数据,之后通过读取即可,匹配IIC协议就行。代码部分不放了,太多了,淘宝买模块,都有的。
也可以使用STM32的时钟RTC进行实时时钟,我这边没有使用。RTC代码正点原子那部分有讲解。B站都有视频。
照明功能
该部分,使用光敏电路部分,通过光敏电阻和电压比较器进行输出。单片机负责检测电压比较器出来的结果,如果是0则开启灯光,如果是1则关闭灯光,基本的模拟电路就可以实现。电路图如下:
如果要是驱动大功率灯,需要外部供电和通过继电器的方式来进行控制。
这边写了一个简单的控制:(控制普通的LED灯作为了模拟)。
代码:
配置对应的光敏信号接口初始化。
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_Init(GPIOB,&GPIO_InitStructure);
GPIO_SetBits(GPIOB,GPIO_Pin_6);
光敏控制部分:B6是对应的光敏接口。通过这个口进行读取光敏信号。PA6接LED灯.
void light_page()
{
OLED_8x16Str(0,0,"light menu");
OLED_8x16Str(0,12,"now light:");
gm_flag=GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_6);
delay_ms(200);
if (gm_flag==0)
{
OLED_8x16Str(96,12,"off");
GPIO_ResetBits(GPIOA,GPIO_Pin_6);
//USART_SendData(USART3,2);
}
else
{
//OLED_8x16Str(96,12,"on ");
//USART_SendData(USART3,1);
GPIO_SetBits(GPIOA,GPIO_Pin_6);
}
}
电压测量:
使用ADC内部的12位高精度的ADC进行电池电压的测量和采集。
考虑到 STM32 上的内部基准电压位 3.3V,最大只可 以测量到 3.3V 的电压,18650 充满电电压达到 4. 2V,因此本设计中使用两个分压电阻 作为分压,在之后程序中根据分压比例乘以对应的分压系数,从而得到对应的电压数值。
为了确保数据转换的稳定性,本设计中使用 ADC 采样时间 239.5 个周期。
其中 SamplTime 是 ADC 采样周期;𝑉是 ADC 采样的内部基准电压为 3.3V,Value 是单片机 ADC 转换后的数值;n 为单片机 ADC 的位数。 之后将其获得的电压值,通过硬件电路上的分压电阻,获得 ADC 计算电压的分压 系数,通过软件算法将获得 ADC 数值计算之后的电压数值乘以分压系数,最后能够获 得当前电池的实际电压,从而实现了 ADC 电压测量范围的扩大。
代码部分:
#include "includes.h"
void Dianya_Init(void)
{
ADC_InitTypeDef ADC_InitStructure;
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA |RCC_APB2Periph_ADC1 , ENABLE ); //使能ADC1通道时钟
RCC_ADCCLKConfig(RCC_PCLK2_Div6); //设置ADC分频因子6 72M/6=12,ADC最大时间不能超过14M
//PA1 作为模拟通道输入引脚
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN; //模拟输入引脚
GPIO_Init(GPIOA, &GPIO_InitStructure);
ADC_DeInit(ADC1); //复位ADC1
ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; //ADC工作模式:ADC1和ADC2工作在独立模式
ADC_InitStructure.ADC_ScanConvMode = DISABLE; //模数转换工作在单通道模式
ADC_InitStructure.ADC_ContinuousConvMode = DISABLE; //模数转换工作在单次转换模式
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; //转换由软件而不是外部触发启动
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; //ADC数据右对齐
ADC_InitStructure.ADC_NbrOfChannel = 1; //顺序进行规则转换的ADC通道的数目
ADC_Init(ADC1, &ADC_InitStructure); //根据ADC_InitStruct中指定的参数初始化外设ADCx的寄存器
ADC_Cmd(ADC1, ENABLE); //使能指定的ADC1
ADC_ResetCalibration(ADC1); //使能复位校准
while(ADC_GetResetCalibrationStatus(ADC1)); //等待复位校准结束
ADC_StartCalibration(ADC1); //开启AD校准
while(ADC_GetCalibrationStatus(ADC1)); //等待校准结束
// ADC_SoftwareStartConvCmd(ADC1, ENABLE); //使能指定的ADC1的软件转换启动功能
}
//获得ADC值
//ch:通道值 0~3
u16 Get_Adc(u8 ch)
{
//设置指定ADC的规则组通道,一个序列,采样时间
ADC_RegularChannelConfig(ADC1, ch, 1, ADC_SampleTime_239Cycles5 ); //ADC1,ADC通道,采样时间为239.5周期
ADC_SoftwareStartConvCmd(ADC1, ENABLE); //使能指定的ADC1的软件转换启动功能
while(!ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC ));//等待转换结束
return ADC_GetConversionValue(ADC1); //返回最近一次ADC1规则组的转换结果
}
u16 Get_Adc_Average(u8 ch,u8 times)
{
u32 temp_val=0;
u8 t;
for(t=0;t<times;t++)
{
temp_val+=Get_Adc(ch);
delay_ms(5);
}
return temp_val/times;
}
心率检测部分:
使用了pulse sensor
原理:心率指的是人在一分钟以内心跳的次数,得到的心率数值最简单的方法就是计数一 个人在一分钟之内检测到了多少次的脉搏数。如果本设计采用这种方法,那么将无法及 时的得到心率数值,而且这种方法采集心率效率特别低。因此本设计采用了另外一种方 式来实时的采集心率数值:测量两次脉搏的时间间隔,之后用一分钟除以两次脉冲信号 之间的时间差,间接得到个体的心率实时数据。 首先计算实时心率数值要引入 IBI 和 BPM 两个概念,其中 IBI 指代两次脉搏之间 特征点的时间间隔数值,BPM 指代的是一个人在一分钟之内的心跳次数。并且两者之间 的换算关系为:BPM = 60/IBI。
脉搏信号波形图
脉搏信号阈值波形图
通过检测脉搏信号获得到的则是一个波形图,但是计算则需要的是一个 IBI 的数值 点,因此需要通过从一段一段的波形中选取一个点,称之为该脉搏信号上的特征点。这 一个脉搏信号特征点代表了一个有效的脉搏信号,通过检测两个相邻的特征点并且做差, 则能够获得到对应的 IBI 数值,之后通过 IBI 和 BPM 两者之间的换算关系从而能够实 时的获得到当前心率值。在本设计中则是选取当信号上升到振幅的一半时,将该点作为 了这一个脉冲信号的特征点。
代码部分: 通过ADC来进行脉冲之间的信号采集,之后做动态阈值的检测,之后通过以上算法来对数据进行处理。(这里一般处理出来的信号很飘的,需要滤波,并且波形图容易收到很大的影响)。
为啥不用MAX30102,我试过那个官方算法,数据完全不稳定的。可能是我实力有限,个人问题把。
Pulse sensor结果:
没有检测到信号时:
结果展示:
上位机部分,以后再讲咋做。图我先不放了,因为有我个人的信息在上面。emmm以后在更新。以下是各个功能的结果图。如果有问题的,可以问我,我有空会解答的。最近摸的嵌入式内容比较少,大部分时间在做深度学习和机器学习部分的。
代码部分:我不提供完成的全套代码, 因为代码中,涉及到我的许多个人信息,不太方便,提供大家一个思路。