项目代码:链接: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站尚硅谷老师做的