心电监测项目

项目代码:链接:https://pan.baidu.com/s/1LDRkckGVnmYZU9_kxut-HA?pwd=jyhh 
提取码:jyhh

1.项目内容

本项目使用心电采集模块将数据发送给上位机在上位机上显示;并且上位机能下发指令采集特定的数据。

2.实验思路

本实验使用心电采集模块(AD8232传感器+外部电路构成)将数据传给ADC进行转换,定时器根据上位机发布的命令(上位机通过串口发送命令),在一定时间内采集数据通过串口发送到上位机显示。本实验分为三层来进行代码的编写:(1)硬件驱动层:串口、定时器、ADC的驱动;(2)应用层:通信模块、心电数据采集模块;(3)公共层:调试代码段。

3.实验遇到的问题

3.1source Browser: 'memcpy' - undefined Definition/Reference !

问题描述:memcpy函数能用,但是在keil中找不到它的定义,为什么(至今还没解决)

3.2内存泄漏

问题描述:设置的全局变量g_communicationBuff内存将g_rate的值覆盖,解决办法是将g_communicationBuff的内存设置的大一点(该问题由b站的老师解决)

3.3最后发送给上位机的数据一直换不了行

(十进制换不了行十六进制显示换行)---好像是串口调试助手的问题

4.实验过程(寄存器开发)

4.1硬件驱动层

4.1.1串口驱动

本实验采用的是串口1,硬件原理图如下4-1所示:

图4-1 USART1原理图

由上图分析可知:PA9复用推挽输出 PA10 浮空输入

串口初始化函数内容:1.开时钟;2.配置GPIO工作模式;3.串口配置:波特率、数据位、奇偶校验位、停止位;4.使能串口;5.因为本项目接收数据要采用的是中断的方法,所以要开接收非空的中断(判断有无接收数据)和空闲中断(判断接收数据是否完成)6.配置NVIC;具体代码实现如下:

void Driver_USART1_Init(void)
{
    //开时钟
    RCC->APB2ENR |= RCC_APB2ENR_USART1EN;
    RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;
    //配置引脚工作模式  TX--PA9 复用推挽 CNF=10 MODE=11   RX--PA10 浮空输入 CNF=01 MODE=00
    GPIOA->CRH |= (GPIO_CRH_MODE9 | GPIO_CRH_CNF9_1);
    GPIOA->CRH &= ~GPIO_CRH_CNF9_0;
    GPIOA->CRH &= ~(GPIO_CRH_MODE10 | GPIO_CRH_CNF10_1);
    GPIOA->CRH |= GPIO_CRH_CNF10_0;
    //波特率 115200
    USART1->BRR = 0x271;
    //配置一个字的长度 8位 CR1_M=0
    USART1->CR1 &= ~USART_CR1_M;
    //配置不校验 CR1_PCE=0
    USART1->CR1 &= ~USART_CR1_PCE;
    //配置停止位的长度 1个停止位 CR2_STOR=00
    USART1->CR2 &= ~USART_CR2_STOP;
    //使能发送 CR1_TE=1
    USART1->CR1 |= USART_CR1_TE;
    //接收使能
    USART1->CR1 |= USART_CR1_RE;
    //使能接收中断RXNEIE=1和空闲中断
    USART1->CR1 |= (USART_CR1_IDLEIE | USART_CR1_RXNEIE);
    //配置NVIC
    NVIC_SetPriorityGrouping(3);
    NVIC_SetPriority(USART1_IRQn,3);
    NVIC_EnableIRQ(USART1_IRQn);

}

串口中断服务函数:1.判断接收非空中断是否开启,开启代表有数据传输,将数据存储到缓冲区buff中,用count记录接受数据的长度;2.判断空闲中断标志位,看是否接收完成,接收完成之后需要执行一个具体的逻辑,我们就先写一个弱函数,到时候谁需要覆写它就行,count清零;具体代码实现如下所示:

//如果需要处理上次接收到的字符,只需要复写这个方法就可以了
__weak void USART1_RxIdleCallBack(uint8_t rxBuff[], uint16_t rxCount)
{

}
uint8_t buff[100] = {0};
uint8_t count = 0;
/**
* 创建时间:2024/08/19 14:15:33
* @description:串口中断服务函数
* @author:zlh
*/
void USART1_IRQHandler(void)
{
    //驱动层应该把数据接收存储到对应的缓冲区,然后再去调用弱函数,把缓冲区传给弱函数
    if(USART1->SR & USART_SR_RXNE)
    {
        buff[count] = USART1->DR & 0xff;
        count++;
    }
    else if(USART1->SR & USART_SR_IDLE)
    {
        //清楚空闲中断标志位
        USART1->SR;
        USART1->DR;
        //执行具体的逻辑
        USART1_RxIdleCallBack(buff, count);
        count = 0;
    }
}

4.1.2ADC驱动

本实验采用的是ADC1的12通道,原理图如下4-2所示

图4-2 ADC1原理图

由上图可知:PC2引脚配置为模拟输入

ADC初始化函数:1.开时钟;2.配置GPIO工作模式;3.配置ADC:转换模式、数据对齐方式、选择触发方式、选择ADC通道、设置采样周期等,具体实现如下所示:

void Driver_ADC1_Init(void)
{
    //1.开时钟
    RCC->APB2ENR |= RCC_APB2ENR_ADC1EN;
    RCC->APB2ENR |= RCC_APB2ENR_IOPCEN;

    //配置PC2 模拟输入 MODE=00 CFN=00
    GPIOC->CRL &= ~(GPIO_CRL_CNF2 | GPIO_CRL_MODE2);

    //禁用扫描模式
    ADC1->CR1 &= ~ADC_CR1_SCAN;
    //启用连续转换
    ADC1->CR2 |= ADC_CR2_CONT;
    //数据对齐方式:右对齐
    ADC1->CR2 &= ~ADC_CR2_ALIGN;
    //禁用规则通道的外部触发
    ADC1->CR2 &= ADC_CR2_EXTTRIG;
    //使用软件触发
    ADC1->CR2 |= ADC_CR2_EXTSEL;
    //规则组有几个通道:1
    ADC1->SQR1 &= ~ADC_SQR1_L;
    //配置规则通道组的通道  使用12通道
    ADC1->SQR3 &= ~ADC_SQR3_SQ1;
    ADC1->SQR3 = 12;
    //设置采样周期  13.5周期 010
    ADC1->SMPR1 &= ~(ADC_SMPR1_SMP12_2 | ADC_SMPR1_SMP12_0);
    ADC1->SMPR1 |= ADC_SMPR1_SMP12_1;
}
 

启动ADC函数 :要上电、校准、启动ADC、开启规则通道开始转换数据,具体代码实现如下:

void Driver_ADC1_Start(void)
{
    //1.上电
    ADC1->CR2 |= ADC_CR2_ADON; 
    //2.校准
    ADC1->CR2 |= ADC_CR2_CAL;
    while(ADC1->CR2 & ADC_CR2_CAL)
    {
        ;
    }
    //3.使能,开始转换
    ADC1->CR2 |= ADC_CR2_ADON; 
    //4.规则通道启动转换
    ADC1->CR2 |= ADC_CR2_SWSTART;
    //5.等待首次转换完成
    while((ADC1->SR & ADC_SR_EOC) == 0);

}
 

4.1.3定时器驱动

本项目选择的是基本定时器TIM7

TIM7的初始化函数:1.开时钟;2.配置定时器TIM7:设置预分频器和自动重装载寄存器的值(这两个值决定了定时时长);3.设置UG位,清除中断标志位(目的是为了避免程序一开始就进入中断);4.开启更新中断服务,配置NVIC,具体代码如下所示:

void Driver_TIM7_Init_(void)
{
    //1.开启时钟
    RCC->APB1ENR |= RCC_APB1ENR_TIM7EN;
    //2.预分频的值
    TIM7->PSC = 7200 - 1;
    //3.重装载寄存器的值  中断的周期是1ms
    TIM7->ARR = 10 - 1;
    //4.设置UG位,触发一次更新事件
    TIM7->EGR |= TIM_EGR_UG;
    //5.清除中断标志位
    TIM7->SR &= ~TIM_SR_UIF;
    //6.开启更新中断
    TIM7->DIER |= TIM_DIER_UIE;
    //7.配置NVIC  优先级组串口出已经配置过,不用再配置
    NVIC_SetPriority(TIM7_IRQn,2);
    NVIC_EnableIRQ(TIM7_IRQn);
}

更新中断服务函数:1.清除中断标志位,2.因为每一毫秒中断一次,所以可以写一个弱函数执行定时1ms之后的操作 ,具体代码实现如下:

//ms中断回调函数
__weak void TIM7_UpInterruptMsCallBack(uint32_t msCount)
{

}
/**
* 创建时间:2024/08/22 16:34:54
* @description:更新中断服务函数
* @author:zlh
*/
uint32_t msCount = 0;
void TIM7_IRQHandler(void)
{
    //清除中断标志位
    TIM7->SR &= ~TIM_SR_UIF;
    //每1毫秒产生一次中断
    msCount++;
    TIM7_UpInterruptMsCallBack(msCount);
}

4.2应用层

4.2.1通信模块

通信模块要实现两个功能:1.接收解析上位机的命令2.将接收的数据传给上位机

1.接收解析上位机的命令:思路是定义三个全局变量,一个标志位、一个缓冲区接收采样率、一个缓冲区接收采样时长,根据命令的形式分析可知第一个字符为s的是采样率;第一个字符为c的是采样时长,将数据分别存储到缓冲区后,将标志位置1表示接收命令完成,再将命令处理一下,具体代码实现如下:

uint8_t g_isReceievCompleted = 0;  //标志位:表示所有参数接收完毕
uint8_t g_receiveRate[10] = {0};   //存储采样率
uint8_t g_receiveDuration[10] = {0};  //存储采样时长
void USART1_RxIdleCallBack(uint8_t rxBuff[], uint16_t rxCount)
{
    if(rxBuff[0] == 's')  //下发的是采样率
    {
       memcpy(g_receiveRate,&rxBuff[1],rxCount-2);
       /*for(uint8_t i = 1; i < rxCount - 2; i++)
        {
            g_receiveRate[i-1] = rxBuff[i];
        }*/
    }
    else if(rxBuff[0] == 'c')  //下发的是采样时长
    {
        memcpy(g_receiveDuration,&rxBuff[1],rxCount-2);
        /*for(uint8_t i = 1; i < rxCount - 2; i++)
        {
            g_receiveDuration[i-1] = rxBuff[i];
        }*/
        g_isReceievCompleted = 1;
    }
}

/**
* 创建时间:2024/08/19 14:59:03
* @description:处理用户指令
* @param:uint16_t* rate  采样率 表示一分钟采样的次数
* @param:uint16_t* duration  采样时间 单位是s
* @return{}
* @author:zlh
*/
void APP_Communication_CommandProcess(uint16_t* rate, uint16_t* duration)
{
    //等待用户下发指令
    while(!g_isReceievCompleted)
    {
        ;
    }
    //2.处理指令
    //2.1把字符变成数字
    *rate = atoi((char*)g_receiveRate);
    *duration = atoi((char*)g_receiveDuration);
}
 

2.将接收的数据传给上位机:将接收到的数据处理后调用串口发送给上位机,具体代码实现如下:

/**
* 创建时间:2024/08/22 21:12:20
* @description:给上位机发送数据
* @param:uint16_t heartData 心电数据
* @author:zlh
*/
uint8_t g_communicationBuff[6] = {0};
void APP_Communication_SendHeartData(uint16_t heartData)
{
    sprintf((char*)g_communicationBuff,"%04lu\n",heartData);  //sprintf函数将格式化的数据写入到字符串中
    Driver_USART1_SendString(g_communicationBuff,5);
}

4.2.2心电数据采集模块

我们覆写定时器的ms中断服务函数,读取ADC的值,就相当于1ms读取一次心电数据,具体代码实现如下:

__IO uint8_t g_isToReadValue = 0;
//覆写定时器的ms中断回调函数
void TIM7_UpInterruptMsCallBack(uint32_t msCount)
{
    if(msCount >= g_duration)
    {
        //关闭定时器
        Driver_TIM7_Stop();
        //停止ADC
        Driver_ADC1_Stop();
    }
    if((msCount % g_rate) == 0)
    {
        //读取ADC的值
        g_isToReadValue = 1;

    }
}

/**
* 创建时间:2024/08/22 20:50:52
* @description:读取心电数据
* @return{} 返回值就是ADC的值
* @author:zlh
*/
uint16_t App_HeartCollect_ReadHeartData(void)
{
    while(g_isToReadValue == 0)
    {
        ;
    }
    g_isToReadValue = 0;
    return Driver_ADC1_ReadValue();
}

4.3公共层

调试模块

调试模块是为了避免之后上位机接收数据时,串口1功能的可选择性(此串口1即能接收数据,还能实现调试功能),具体代码实现如下图4-3所示:

图4-3 调试模块代码展示

5.实验总结

完成本项目之后我首先总结学到了如何合理地使用debug层;写代码的过程中我也学到了一些c语言标准函数的使用比如memcpy,sprintf等,也更清晰的项目最初规划时分层的必要性,它能帮助你更直观的看懂整个项目的脉络,也更理解了以前不太懂的知识点。如果上述内容有误,或者有能优化的地方,欢迎指教~~

注:本项目我是跟着b站尚硅谷老师做的

  • 27
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值