• 首页 首页 icon
  • 工具库 工具库 icon
    • IP查询 IP查询 icon
  • 内容库 内容库 icon
    • 快讯库 快讯库 icon
    • 精品库 精品库 icon
    • 问答库 问答库 icon
  • 更多 更多 icon
    • 服务条款 服务条款 icon

Note10STM32H7+HAL+CubeMX+DMA+SPI+串口断+定时器+RTC的多传感器数据采集系统2*ADXL355和ADXL375通过Sync时序同步

武飞扬头像
wql_njust
帮助1

本文的初衷一方面是将我的一些关于STM32开发方面浅显的个人经验分享给初学者、并期望得到大佬的批评指正,另一方面是记录自己的实验过程便于回顾。

我预感应该要写很多,不过鉴于之前的数篇笔迹中,对于SPI/DMA/ADXL3XX系列加表的使用已经详细描述过了,所以这篇博客只记录系统构建的整体流程。

摘要:

通过STM32H743VIT6驱动两片adxl355和1片adxl375,采用SYNC信号同步控制方式实现3个传感器的数据,采用FIFO流模式,采用3组SPI DMA实现数据的同步采集,采用串口1 DMA进行数据传输,采用串口2 中断 构建指令系统,具体指令及对应的功能如下图。通过 定时器 计数 实现了频率可调的方波信号和周期可调的中断,通过RTC产生的秒中断实现定时采样、实际采样率检测 的功能,此外还可以自定义帧标记、获取温度 等功能。

学新通

0 硬件电路介绍

本文基于紫色的板子进行介绍,原理图和细节就不放了,需要用到的时候会指明,总体用到的外设有哪些呢?见下面cubeMX的引脚分配图

学新通
学新通

1 实现3组SPI DMA,并进行数据采集

3组SPI DMA的基本配置都很简单(且相同),前面的文章都详细介绍过,因此直接上图,

学新通
学新通
学新通

由于SPI DMA的传输是非阻塞的,意思就是,如果我写了“SPI接收”,然后“片选拉高”,程序执行完SPI接收后,不管DMA是否传输完毕,都会继续执行“片选拉高”,这是不科学的,一种常见的解决方案是增加延时,但是这样做就失去了DMA的意义,既然有时间延时,为啥要用DMA?而且这种方式无法实现3组SPI同时接收(会差数个机器周期,忽略不计吧)

我的解决方案是:采用标志位的思想,如下,当spix(指spi1或2或3)在发送或接收的时候,将对应的标志位置位,3组spi在同时工作,这个时候,我不断查询标志位,如果3个标志位都回到 0 ,就可以继续使用SPI了,否则等待。

  1.  
    volatile uint8_t DMA_FLAG=0x7f;
  2.  
    //Reserved | usart | spi1TX | spi2TX | spi3TX | spi1RX | spi2RX | spi3RX

因此,3个传感器的数据同步采集函数如下,具体细节:SAMPLE_START;SAMPLE_ADDRESS;...这些抽象的东西是宏定义,即 拉高3个片选, 分别对3片传感器发送数据读取的地址,...,Delay_us是微秒延时函数,在前面的文章有介绍。

  1.  
    void data_sample(){
  2.  
    SAMPLE_START;
  3.  
    Delay_us(5);
  4.  
    DMA_FLAG&=0xc7;//spi send busy ,bit 0 is busy
  5.  
    SAMPLE_ADDRESS;
  6.  
    while((DMA_FLAG&0x38)!=0x38){Delay_us(5);}// wait until address is sent
  7.  
    DMA_FLAG&=0xf8;//spi receive busy
  8.  
    SAMPLE_RECEIVE;
  9.  
    while((DMA_FLAG&0x07)!=0x07){Delay_us(5);}// wait until data is received
  10.  
    SAMPLE_END;
  11.  
    while((DMA_FLAG&0x40)!=0x40){Delay_us(5);} // wait until uasrt1 is ready
  12.  
    DMA_FLAG&=0xbf; //usart1 set busy
  13.  
    HAL_UART_Transmit_DMA(&huart1,SPI_RX_Buffer, 29);
  14.  
    Delay_us(80);
  15.  
    }
学新通

为了怕难以理解,宏定义如下:

  1.  
    #define SAMPLE_START HAL_GPIO_WritePin(XL355_CS_GPIO_Port, XL355_CS_Pin, GPIO_PIN_RESET);\
  2.  
    HAL_GPIO_WritePin(XL355_2_CS_GPIO_Port, XL355_2_CS_Pin, GPIO_PIN_RESET);\
  3.  
    HAL_GPIO_WritePin(XL357_CS_GPIO_Port, XL357_CS_Pin, GPIO_PIN_RESET)
  4.  
    #define SAMPLE_END HAL_GPIO_WritePin(XL355_CS_GPIO_Port, XL355_CS_Pin, GPIO_PIN_SET);\
  5.  
    HAL_GPIO_WritePin(XL355_2_CS_GPIO_Port, XL355_2_CS_Pin, GPIO_PIN_SET);\
  6.  
    HAL_GPIO_WritePin(XL357_CS_GPIO_Port, XL357_CS_Pin, GPIO_PIN_SET)
  7.  
    #define SAMPLE_ADDRESS HAL_SPI_Transmit_DMA(&hspi1, &SPI_READ_DATA_Address, 1);\
  8.  
    HAL_SPI_Transmit_DMA(&hspi2, &SPI_READ_DATA_Address, 1);\
  9.  
    HAL_SPI_Transmit_DMA(&hspi3, &SPI_READ_DATA_Address, 1)
  10.  
    #define SAMPLE_RECEIVE HAL_SPI_Receive_DMA(&hspi1, &SPI_RX_Buffer[0], 9);\
  11.  
    HAL_SPI_Receive_DMA(&hspi2, &SPI_RX_Buffer[9], 9);\
  12.  
    HAL_SPI_Receive_DMA(&hspi3, &SPI_RX_Buffer[18], 9)

下面是DMA回调函数的写法,一共6个,只给出两个,其他的同理。这些回调函数在stm32h7xx_it.c里,只有 DMA_FLAG|=0x04;是我写进去的,写在处理中断前/后 都可以,目的就是给标志位置位。

  1.  
    void DMA1_Stream0_IRQHandler(void)
  2.  
    {
  3.  
    /* USER CODE BEGIN DMA1_Stream0_IRQn 0 */
  4.  
    //for SPI1-RX
  5.  
    DMA_FLAG|=0x04;
  6.  
    /* USER CODE END DMA1_Stream0_IRQn 0 */
  7.  
    HAL_DMA_IRQHandler(&hdma_spi1_rx);
  8.  
    /* USER CODE BEGIN DMA1_Stream0_IRQn 1 */
  9.  
     
  10.  
    /* USER CODE END DMA1_Stream0_IRQn 1 */
  11.  
    }
  12.  
     
  13.  
    /**
  14.  
    * @brief This function handles DMA1 stream1 global interrupt.
  15.  
    */
  16.  
    void DMA1_Stream1_IRQHandler(void)
  17.  
    {
  18.  
    /* USER CODE BEGIN DMA1_Stream1_IRQn 0 */
  19.  
    //for SPI1-TX
  20.  
    DMA_FLAG|=0x20;
  21.  
    /* USER CODE END DMA1_Stream1_IRQn 0 */
  22.  
    HAL_DMA_IRQHandler(&hdma_spi1_tx);
  23.  
    /* USER CODE BEGIN DMA1_Stream1_IRQn 1 */
  24.  
     
  25.  
    /* USER CODE END DMA1_Stream1_IRQn 1 */
  26.  
    }
学新通

2 实现usart DMA/中断,进行数据传输和菜单设置

开了两路usart,其中usart1用DMA方式来传输数据 1500000波特率(用镀银的杜邦线,极度奢侈),usart2用中断方式来接收指令,并用常规方式来打印信息,配置极为简单(只改波特率,其余默认)不上图了,需要注意的是,usart2用中断方式来接收指令因此中断的优先级可以高一点,比SPI DMA(优先级3)略高,暂时设定为2,然后更高的优先级1 留给定时器和RTC的秒中断。

usart1用DMA方式来传输数据在上一部分的sample函数里,一行代码。

usart2的中断函数是这样的,几点细节:switch语句的查表方式比多个if连用的效率更高,串口接收一次之后,要重新执行接收函数才能再次接收。下面的代码应该是浅显易懂的

  1.  
    void USART2_IRQHandler(void)
  2.  
    {
  3.  
    /* USER CODE BEGIN USART2_IRQn 0 */
  4.  
     
  5.  
    /* USER CODE END USART2_IRQn 0 */
  6.  
    HAL_UART_IRQHandler(&huart2);
  7.  
    /* USER CODE BEGIN USART2_IRQn 1 */
  8.  
     
  9.  
    switch(UART_RX){
  10.  
    case 0x00:{
  11.  
    SAMPLE_FLAG^=0x80;
  12.  
    if(SAMPLE_FLAG&0x80)
  13.  
    puts("Sampling...");
  14.  
    else
  15.  
    puts("Pause...");
  16.  
    break;
  17.  
    }
  18.  
    case 0x01:SR_Counter_RES=800;puts("Set SR = 5Hz");break;
  19.  
    case 0x02:SR_Counter_RES=80;puts("Set SR = 50Hz");break;
  20.  
    case 0x03:SR_Counter_RES=40;puts("Set SR = 100Hz");break;
  21.  
    case 0x04:SR_Counter_RES=20;puts("Set SR = 200Hz");break;
  22.  
    case 0x05:SR_Counter_RES=8;puts("Set SR = 500Hz");break;
  23.  
    case 0x06:SR_Counter_RES=5;puts("Set SR = 800Hz");break;
  24.  
    case 0x07:SR_Counter_RES=4;puts("Set SR = 1000Hz");break;
  25.  
    case 0x08:SR_Counter_RES=2;puts("Set SR = 2000Hz");break;
  26.  
    case 0x0a:{
  27.  
    SAMPLE_FLAG^=0x40;
  28.  
    if(SAMPLE_FLAG&0x40)
  29.  
    puts("Time_Count mark : ON");
  30.  
    else{
  31.  
    puts("Time_Count mark : OFF");
  32.  
    SAMPLES=0;
  33.  
    }
  34.  
    break;
  35.  
    }
  36.  
     
  37.  
    case 0x10:{
  38.  
    SAMPLE_FLAG&=0x7e;
  39.  
    Timing=0;
  40.  
    puts("Timing sample is OFF ");
  41.  
    break;
  42.  
    }
  43.  
    case 0x11:{
  44.  
    SAMPLE_FLAG|=0x01;
  45.  
    SAMPLE_FLAG&=0x7f;
  46.  
    Timing=30;
  47.  
    puts("Timing sample : 30s, enter 0x00 to start.");
  48.  
    break;
  49.  
    }
  50.  
    case 0x12:{
  51.  
    SAMPLE_FLAG|=0x01;
  52.  
    SAMPLE_FLAG&=0x7f;
  53.  
    Timing=60;
  54.  
    puts("Timing sample : 60s, enter 0x00 to start.");
  55.  
    break;
  56.  
    }
  57.  
    case 0x13:{
  58.  
    SAMPLE_FLAG|=0x01;
  59.  
    SAMPLE_FLAG&=0x7f;
  60.  
    Timing=180;
  61.  
    puts("Timing sample : 180s, enter 0x00 to start.");
  62.  
    break;
  63.  
    }
  64.  
    case 0x14:{
  65.  
    SAMPLE_FLAG|=0x01;
  66.  
    SAMPLE_FLAG&=0x7f;
  67.  
    Timing=300;
  68.  
    puts("Timing sample : 300s, enter 0x00 to start.");
  69.  
    break;
  70.  
    }
  71.  
     
  72.  
    case 0x20:{
  73.  
    SAMPLE_START;
  74.  
    HAL_SPI_Transmit(&hspi1, &SPI_Temp_Address, 1,0xffff);
  75.  
    HAL_SPI_Transmit(&hspi2, &SPI_Temp_Address, 1,0xffff);
  76.  
    HAL_SPI_Transmit(&hspi3, &SPI_Temp_Address, 1,0xffff);
  77.  
    HAL_SPI_Receive(&hspi1, Temperture, 2,0xffff);
  78.  
    HAL_SPI_Receive(&hspi2, &Temperture[2], 2,0xffff);
  79.  
    HAL_SPI_Receive(&hspi3, &Temperture[4], 2,0xffff);
  80.  
    SAMPLE_END;
  81.  
    puts("Temperture in HEX(2Bytes*3):");
  82.  
    printf("%x %x %x %x %x %x\n\n",Temperture[0],Temperture[1],Temperture[2],Temperture[3],Temperture[4],Temperture[5]);
  83.  
    break;
  84.  
    }
  85.  
     
  86.  
    case 0x30:{
  87.  
    puts("Enter User-Mark(1 Byte in HEX, must be confined in 0xE0~0xFF): ");
  88.  
    HAL_UART_Receive(&huart2,&SPI_RX_Buffer[28],1,0xffff);
  89.  
    SPI_RX_Buffer[28]=SPI_RX_Buffer[28]<0xE0? 0x55:SPI_RX_Buffer[28];
  90.  
    printf("User-Mark is %x\n",SPI_RX_Buffer[28]);
  91.  
    break;
  92.  
    }
  93.  
    default:puts("Don't have this Commond!");break;
  94.  
    }
  95.  
    HAL_UART_Receive_IT(&huart2,&UART_RX,1);
  96.  
    /* USER CODE END USART2_IRQn 1 */
  97.  
    }
学新通

3 实现定时器产生固定频率的SYNC信号,并进一步实现可调的采样率

关于定时器,我也是在博客上现学的,首先时钟树如下:

学新通

可见 右边写着:to APB2 Timer clocks 是240MHz,我将用到的Timer3就是在这个时钟域。Tim3的配置如下,开启通道1输出 作为SYNC信号,Tooggle on match 就是每次计数器溢出时翻转输出电平,根据图中的参数来计算,Tim3的溢出频率=240Mhz/(预分频系数 1)/(计数周期 1)=240M/(59 1)/(999 1)=4Khz。因此CH1输出的SYNC信号为2Khz的方波信号。Tim3中断函数的调用频率为4kHz。

学新通

开启Tim3的中断,

学新通

在用MX_TIM3_Init函数初始化完成后,用下面两行代码分别开启TIM3基本定时器和TIM3的CH1,如果没有第一行,CH1依然有输出,但是中断函数不会被调用。

  1.  
    HAL_TIM_Base_Start_IT(&htim3);
  2.  
    HAL_TIM_OC_Start(&htim3,TIM_CHANNEL_1);

Tim3的中断函数这样写(只有8-13行,其余是自动生成的),每次中断SR_Counter减一,当重装时,置为SAMPLE_FLAG中的采样标志位,程序查询到该标志位时进行采样。这样,就可以通过改变SR_Counter_RES的值,动态修改采样率了,而不需要重新配置TIM3,因为重新配置TIM3也势必会影响SYNC信号,再开一个定时器也没必要。

程序以200khz的频率查询,因此查询和标志位产生之间的误差可以忽略,如果需要进一步增加采样率精度,是否可以把采样函数写在该中断函数里?如果我中断间隔为250us,中断里写的程序需要运行50us,那我的中断间隔会被改变吗?我还没验证,不过采用我目前的实现方式,TIM3中断中的代码量极小,因此不用考虑这样的情况。

注意:我在两次采样间无延时的情况下,全速运行10秒,大约得到了7万个数据,这说明我完成一次3个传感器的采样和传输所需要的时间小于0.2ms,因此我通过上述的方式实现最大采样率为2000Hz是完全OK的。

  1.  
    void TIM3_IRQHandler(void)
  2.  
    {
  3.  
    /* USER CODE BEGIN TIM3_IRQn 0 */
  4.  
     
  5.  
    /* USER CODE END TIM3_IRQn 0 */
  6.  
    HAL_TIM_IRQHandler(&htim3);
  7.  
    /* USER CODE BEGIN TIM3_IRQn 1 */
  8.  
    if(SR_Counter)
  9.  
    SR_Counter--;
  10.  
    else{
  11.  
    SR_Counter=SR_Counter_RES-1;
  12.  
    SAMPLE_FLAG|=0x08;
  13.  
    }
  14.  
     
  15.  
    /* USER CODE END TIM3_IRQn 1 */
  16.  
    }
学新通

主函数的主循环中,采样的部分这样写:

  1.  
    if(SAMPLE_FLAG&0x80){
  2.  
    while((SAMPLE_FLAG&0x08)!=0x08){Delay_us(5);}
  3.  
    SAMPLE_FLAG&=0xf7;
  4.  
    data_sample();
  5.  
    SAMPLES=SAMPLE_FLAG&0x40?SAMPLES 1:SAMPLES;
  6.  
    if ((SAMPLE_FLAG&0x01)&& !Timing){
  7.  
    SAMPLE_FLAG&=0x7e;
  8.  
    puts("Timing sample is done!");
  9.  
    }

其中的SAMPLE_FLAG是采样标志位,具体分配如下:(除去保留位,从左到右分别为:采样启停、时间-采样数的水印开关、采样标志位、定时采样标志位)

  1.  
    volatile uint8_t SAMPLE_FLAG=0;
  2.  
    //RUN | Time_Count_MARK | Reserved | Reserved | Sample | Reserved | Reserved | Timing

4 实现RTC产生秒中断,并进一步实现定时采样和实际采样率检测

RTC这地方我调了很久,因为经常RTC调着调着就不工作了,原因是: 我的板子没有复位按键,是上电自动复位,程序下载之后,如果板子不重新上电,32.768k的晶振就很有可能无法起振。

RTC产生秒中断,配置如下,internal wakeup意思是内部中断(不输出),wakeup clock 1hz很方便,自动产生秒信号。

学新通
学新通

中断函数如下,RTC_Second是我用来计秒的(从上电到现在的秒数),RTC应该有专门的秒寄存器,但是我懒得找了。LED0_Toggle是宏定义,翻转LED灯的;如果设置了定时采样,采样会暂停,当采样开始的时候,每过一秒Timing(定时的秒数)减一;如果开启了时间-采样数水印,会打印当前的秒数和当前累计采集到的样本SAMPLES,这样就可以看到实际的采样率,例如下下面的图,采样率设置为800hz,实际大约是779.5hz,当然这个误差是可以通过优化代码来进一步缩小的。

  1.  
    void RTC_WKUP_IRQHandler(void)
  2.  
    {
  3.  
    /* USER CODE BEGIN RTC_WKUP_IRQn 0 */
  4.  
     
  5.  
    /* USER CODE END RTC_WKUP_IRQn 0 */
  6.  
    HAL_RTCEx_WakeUpTimerIRQHandler(&hrtc);
  7.  
    /* USER CODE BEGIN RTC_WKUP_IRQn 1 */
  8.  
    LED0_Toggle;
  9.  
    RTC_Second ;
  10.  
    if((SAMPLE_FLAG&0x81)==0x81){
  11.  
    Timing--;
  12.  
    printf("Timing rest seconds : M\n",Timing);
  13.  
    }
  14.  
    if(SAMPLE_FLAG&0x40)
  15.  
    printf("SAMPLES : d, Power-seconds ]\n",SAMPLES,RTC_Second);
  16.  
     
  17.  
    /* USER CODE END RTC_WKUP_IRQn 1 */
  18.  
    }
学新通
学新通

ok,到此为止了,下面展示一下开机界面

学新通

代码多逻辑复杂,贴不尽。如果需要进一步讨论该工程的完整代码请私信或评论留言。欢迎讨论!

这篇好文章是转载于:学新通技术网

  • 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
  • 本站站名: 学新通技术网
  • 本文地址: /boutique/detail/tanhgggegg
系列文章
更多 icon
同类精品
更多 icon
继续加载