本文的初衷一方面是将我的一些关于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” />我的解决方案是:采用标志位的思想,如下,当spix(指spi1或2或3)在发送或接收的时候,将对应的标志位置位,3组spi在同时工作,这个时候,我不断查询标志位,如果3个标志位都回到 0 ,就可以继续使用SPI了,否则等待。

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

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

void data_sample(){SAMPLE_START;Delay_us(5);DMA_FLAG&=0xc7;//spi send busy ,bit 0 is busySAMPLE_ADDRESS;while((DMA_FLAG&0x38)!=0x38){Delay_us(5);}//wait until address is sentDMA_FLAG&=0xf8;//spi receive busySAMPLE_RECEIVE;while((DMA_FLAG&0x07)!=0x07){Delay_us(5);}//wait until data is receivedSAMPLE_END;while((DMA_FLAG&0x40)!=0x40){Delay_us(5);} //wait until uasrt1 is readyDMA_FLAG&=0xbf;//usart1 set busyHAL_UART_Transmit_DMA(&huart1,SPI_RX_Buffer, 29);Delay_us(80);}

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

#define SAMPLE_STARTHAL_GPIO_WritePin(XL355_CS_GPIO_Port, XL355_CS_Pin, GPIO_PIN_RESET);\HAL_GPIO_WritePin(XL355_2_CS_GPIO_Port, XL355_2_CS_Pin, GPIO_PIN_RESET);\HAL_GPIO_WritePin(XL357_CS_GPIO_Port, XL357_CS_Pin, GPIO_PIN_RESET)#define SAMPLE_ENDHAL_GPIO_WritePin(XL355_CS_GPIO_Port, XL355_CS_Pin, GPIO_PIN_SET);\HAL_GPIO_WritePin(XL355_2_CS_GPIO_Port, XL355_2_CS_Pin, GPIO_PIN_SET);\HAL_GPIO_WritePin(XL357_CS_GPIO_Port, XL357_CS_Pin, GPIO_PIN_SET)#define SAMPLE_ADDRESSHAL_SPI_Transmit_DMA(&hspi1, &SPI_READ_DATA_Address, 1);\HAL_SPI_Transmit_DMA(&hspi2, &SPI_READ_DATA_Address, 1);\HAL_SPI_Transmit_DMA(&hspi3, &SPI_READ_DATA_Address, 1)#define SAMPLE_RECEIVEHAL_SPI_Receive_DMA(&hspi1, &SPI_RX_Buffer[0], 9);\HAL_SPI_Receive_DMA(&hspi2, &SPI_RX_Buffer[9], 9);\HAL_SPI_Receive_DMA(&hspi3, &SPI_RX_Buffer[18], 9)

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

void DMA1_Stream0_IRQHandler(void){/* USER CODE BEGIN DMA1_Stream0_IRQn 0 *///for SPI1-RXDMA_FLAG|=0x04;/* USER CODE END DMA1_Stream0_IRQn 0 */HAL_DMA_IRQHandler(&hdma_spi1_rx);/* USER CODE BEGIN DMA1_Stream0_IRQn 1 *//* USER CODE END DMA1_Stream0_IRQn 1 */}/*** @brief This function handles DMA1 stream1 global interrupt.*/void DMA1_Stream1_IRQHandler(void){/* USER CODE BEGIN DMA1_Stream1_IRQn 0 *///for SPI1-TXDMA_FLAG|=0x20;/* USER CODE END DMA1_Stream1_IRQn 0 */HAL_DMA_IRQHandler(&hdma_spi1_tx);/* USER CODE BEGIN DMA1_Stream1_IRQn 1 *//* USER CODE END DMA1_Stream1_IRQn 1 */}

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

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

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

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

void USART2_IRQHandler(void){/* USER CODE BEGIN USART2_IRQn 0 *//* USER CODE END USART2_IRQn 0 */HAL_UART_IRQHandler(&huart2);/* USER CODE BEGIN USART2_IRQn 1 */switch(UART_RX){case 0x00:{SAMPLE_FLAG^=0x80;if(SAMPLE_FLAG&0x80)puts("Sampling...");elseputs("Pause...");break;}case 0x01:SR_Counter_RES=800;puts("Set SR = 5Hz");break;case 0x02:SR_Counter_RES=80;puts("Set SR = 50Hz");break;case 0x03:SR_Counter_RES=40;puts("Set SR = 100Hz");break;case 0x04:SR_Counter_RES=20;puts("Set SR = 200Hz");break;case 0x05:SR_Counter_RES=8;puts("Set SR = 500Hz");break;case 0x06:SR_Counter_RES=5;puts("Set SR = 800Hz");break;case 0x07:SR_Counter_RES=4;puts("Set SR = 1000Hz");break;case 0x08:SR_Counter_RES=2;puts("Set SR = 2000Hz");break;case 0x0a:{SAMPLE_FLAG^=0x40;if(SAMPLE_FLAG&0x40)puts("Time_Count mark : ON");else{puts("Time_Count mark : OFF");SAMPLES=0;}break;}case 0x10:{SAMPLE_FLAG&=0x7e;Timing=0;puts("Timing sample is OFF ");break;}case 0x11:{SAMPLE_FLAG|=0x01;SAMPLE_FLAG&=0x7f;Timing=30;puts("Timing sample : 30s, enter 0x00 to start.");break;}case 0x12:{SAMPLE_FLAG|=0x01;SAMPLE_FLAG&=0x7f;Timing=60;puts("Timing sample : 60s, enter 0x00 to start.");break;}case 0x13:{SAMPLE_FLAG|=0x01;SAMPLE_FLAG&=0x7f;Timing=180;puts("Timing sample : 180s, enter 0x00 to start.");break;}case 0x14:{SAMPLE_FLAG|=0x01;SAMPLE_FLAG&=0x7f;Timing=300;puts("Timing sample : 300s, enter 0x00 to start.");break;}case 0x20:{SAMPLE_START;HAL_SPI_Transmit(&hspi1, &SPI_Temp_Address, 1,0xffff);HAL_SPI_Transmit(&hspi2, &SPI_Temp_Address, 1,0xffff);HAL_SPI_Transmit(&hspi3, &SPI_Temp_Address, 1,0xffff);HAL_SPI_Receive(&hspi1, Temperture, 2,0xffff);HAL_SPI_Receive(&hspi2, &Temperture[2], 2,0xffff);HAL_SPI_Receive(&hspi3, &Temperture[4], 2,0xffff);SAMPLE_END;puts("Temperture in HEX(2Bytes*3):");printf("%x %x %x %x %x %x\n\n",Temperture[0],Temperture[1],Temperture[2],Temperture[3],Temperture[4],Temperture[5]);break;}case 0x30:{puts("Enter User-Mark(1 Byte in HEX, must be confined in 0xE0~0xFF): ");HAL_UART_Receive(&huart2,&SPI_RX_Buffer[28],1,0xffff);SPI_RX_Buffer[28]=SPI_RX_Buffer[28]<0xE0? 0x55:SPI_RX_Buffer[28];printf("User-Mark is %x\n",SPI_RX_Buffer[28]);break;}default:puts("Don't have this Commond!");break;}HAL_UART_Receive_IT(&huart2,&UART_RX,1);/* USER CODE END USART2_IRQn 1 */}

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依然有输出,但是中断函数不会被调用。

HAL_TIM_Base_Start_IT(&htim3);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的。

void TIM3_IRQHandler(void){/* USER CODE BEGIN TIM3_IRQn 0 *//* USER CODE END TIM3_IRQn 0 */HAL_TIM_IRQHandler(&htim3);/* USER CODE BEGIN TIM3_IRQn 1 */if(SR_Counter)SR_Counter--;else{SR_Counter=SR_Counter_RES-1;SAMPLE_FLAG|=0x08;}/* USER CODE END TIM3_IRQn 1 */}

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

if(SAMPLE_FLAG&0x80){while((SAMPLE_FLAG&0x08)!=0x08){Delay_us(5);}SAMPLE_FLAG&=0xf7;data_sample();SAMPLES=SAMPLE_FLAG&0x40" />

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

volatile uint8_t SAMPLE_FLAG=0;//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,当然这个误差是可以通过优化代码来进一步缩小的。

void RTC_WKUP_IRQHandler(void){/* USER CODE BEGIN RTC_WKUP_IRQn 0 *//* USER CODE END RTC_WKUP_IRQn 0 */HAL_RTCEx_WakeUpTimerIRQHandler(&hrtc);/* USER CODE BEGIN RTC_WKUP_IRQn 1 */LED0_Toggle;RTC_Second++;if((SAMPLE_FLAG&0x81)==0x81){Timing--;printf("Timing rest seconds : %4d\n",Timing);}if(SAMPLE_FLAG&0x40)printf("SAMPLES : %10d, Power-seconds %5d\n",SAMPLES,RTC_Second);/* USER CODE END RTC_WKUP_IRQn 1 */}

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

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