您现在的位置是:主页 > news > 西安做网站那家公司好/网站建设平台官网
西安做网站那家公司好/网站建设平台官网
admin2025/5/2 22:53:03【news】
简介西安做网站那家公司好,网站建设平台官网,wordpress 不重定向,网站titleStm32F407之SYSTICK滴答计时器、IO复用、DMA串口、延时函数 文章目录Stm32F407之SYSTICK滴答计时器、IO复用、DMA串口、延时函数1.SYSTICK计时器2.定时器3.延时函数的设置4.IO复用5.串口USART1设置6.DMA设置1.SYSTICK计时器 滴答计时器是内核自带的,前面文章已说明…
Stm32F407之SYSTICK滴答计时器、IO复用、DMA串口、延时函数
文章目录
- Stm32F407之SYSTICK滴答计时器、IO复用、DMA串口、延时函数
- 1.SYSTICK计时器
- 2.定时器
- 3.延时函数的设置
- 4.IO复用
- 5.串口USART1设置
- 6.DMA设置
1.SYSTICK计时器
滴答计时器是内核自带的,前面文章已说明其时钟源可以为AHB的1/8,也可以为HCLK,按照本系列文章的约定,AHB=168MHz,HCLK=168Mhz。
这是一个24位的计时器,从重装值数到0,具有中断配置。
下面直接看寄存器,这些在M4内核编程手册上有,如有需要,留下邮箱。
CTRL寄存器里面,位0、2、16是所需要的,不用开启TICKINT中断,查询16位标记即可。
可以清楚看到该寄存器是24位的,上面指出,滴答计时器的中断和COUNTFLAG在从1数到0时激活,所以如果没有这个过程(如重装值是0),则不会触发。
24位最大数16777216次,168Mhz下最大单次延时0.09986s=99.86ms,8分频时最大单次延时798.915ms。
这里强调了单次延时,如果想延时超过798ms,在不改变AHB和HCLK频率的情况下,可以设计多次延时来实现,本文中不考虑。
这里指出滴答计时器的一个问题,那就是不具备函数重载性,例如在一个滴答计数器延时中,中断触发,中断中又调用了滴答计时器,就会重写之前未完成的滴答计数,破坏程序整体延时。也就是说镶嵌调用不安全,当然可通过软件手段,如设计保护区等等来解决这种问题,但本文不考虑。
这是可以查看当前计数值的寄存器,也许可以用这个设计查询计时器。写入任何值可以清零。
下面是设置Systick的代码:
SysTick->CTRL&=~(1<<2); //SYSTICK使用外部时钟源
//这个在程序初始阶段使用一次就好了,这里选的是AHB/8
u32 temp;
SysTick->LOAD=time_value; //时间加载
SysTick->VAL=0x00; //清空计数器
SysTick->CTRL=0x01 ; //开始倒数
//这三步就是关键步骤了
do
{temp=SysTick->CTRL;
}while((temp&0x01)&&!(temp&(1<<16))); //等待时间到达
//等待查询,计数完成后,
SysTick->CTRL=0x00; //关闭计数器
SysTick->VAL =0X00;
使用流程很简单,时间加载值要根据当前时钟频率计算而出。加上外部的软件配套代码,可设计出实际用途的计时器。
2.定时器
stm32F407具有14个定时器,但资源和功用各不相同,这里只介绍计数功能,其他功能这里不考虑。
(1)TIM1和TIM8,高级定时器,16位计数自动重装,分频系数1到65536之间。可递减、递增等。
(2)TIM2和TIM5,通用定时器,32位计数,其他与(1)一样。
(3)TIM3和TIM4,与(1)一样
(4)TIM9-TIM14,16位递增自动重装计数器,分频系数1到65536之间。
(5)TIM6和TIM7,与(4)一样。
注意的是,这些定时器并非都有独立中断。
实际上,只有TIM2/3/4/5/7有独立全局中断,其他都是耦合在一起了,具体看手册。
这里直接给出TIM3的定时中断代码,定时中断配置简单,移植也简单。
//通用定时器3中断初始化
//这里时钟选择为APB1的2倍,而APB1为42M
//arr:自动重装值。
//psc:时钟预分频数
//定时器溢出时间计算方法:Tout=((arr+1)*(psc+1))/Ft us.
//Ft=定时器工作频率,单位:Mhz
//这里使用的是定时器3!
void TIM3_int_init(u16 arr,u16 psc)
{RCC->APB1ENR|=1<<1; //TIM3时钟使能//time2-7,12-14都是使用AP1时钟*2=84Mhz TIM3->ARR=arr; //设定计数器自动重装值 //16位值,当自动重载值为空时,计数器不工作。//当缓冲开启(CR1_APRE=1),则发生更新事件后才写入,否则及时写入TIM3->PSC=psc; //预分频器//16位,计数器时钟频率 CK_CNT 等于 fCK_PSC / (PSC[15:0] + 1)。//PSC 包含在每次发生更新事件时要装载到实际预分频器寄存器的值。 TIM3->DIER|=1<<0; //允许更新中断//TDE=0,默认禁止触发DMA请求//CCxDE,默认禁止捕获/计数 DMA请求//UDE=0,默认禁止更新DMA请求//捕获、计数、触发中断默认关闭//这里开启了更新中断d TIM3->CR1|=1<<2; //将URS位置1,这样将UG置1时不会生成更新请求TIM3->EGR = 0x01;//UG置1,更新寄存器TIM3->CR1 |= 0x01;//使能定时器//CKD=00,默认数字滤波器采样时钟=定时器时钟,时钟分频//APRE=0,默认TIMx_ARR寄存器不进行缓冲。//CMS=00,默认边沿对齐模式//DIR=0,默认计数方向递增,中心对齐和编码器模式只可读//OPM=0,默认发生更新事件不停止计数//URS=0,默认(计数器上下溢,UG置1,从模式控制的更新事件,)可以生成更新中断、以及DMA请求。//UDIS=0,默认使能UEV,可通过(计数器上下溢,UG置1,从模式控制的更新事件)生成更新事件//CEN=1,这里使能了计数器。//TIMX_CR2寄存器看手册,涉及TL1输入选择,主模式选择,DMA选择//TIMX_SMCR寄存器涉及外部触发极性,外部时钟,触发选择等(是从模式控制器)//TIMx_EGR,事件生成寄存器,看手册,默认无操作,含UG ,更新生成,可软件置1。//TIMx_CCMR1、2 捕获/比较模式配置,看手册,TIMx_CCER使能相应配置。//TIMx_CNT,计数器,16位,存储了当前计数值//TIMx_CCRx 捕获比较寄存器,//DMA和选项寄存器看手册MY_NVIC_Init(2,0,TIM3_IRQn,4); //抢占2,子优先级0,组4
}
void TIM3_IRQHandler(void)
{ if(TIM3->SR&0X0001)//溢出中断//该寄存器包含捕捉、比较、触发、更新及重复中断状态标志位。//这里读取更新中断标志,下溢或上溢时会产生(CR1_UDIS应为0)。{Delay_count++;//10ms加一次,总计时为2400万秒} TIM3->SR&=~(1<<0);//清除中断标志位
}
代码注释非常清楚了,具体要对照数据手册来看,配置思路就四步:
(1)RCC使能对应时钟,这个看RCC的寄存器就知道使能哪个位了。
(2)配置重装值和预分频值,这个一起决定定时时长。
(3)配置中断,看需不要开启中断。
(4)清零计数器并使能定时器
中断的注册函数在前一文章中已有说明,SR中有很多中断,其中第0位是更新中断,也就是计数中断。
注册名和中断函数名(TIM3_IRQn、TIM3_IRQHandler)在启动代码startup_stm32f40_41xxx.s中有定义,要严格按定义来,除非你修改该文件中的定义。
3.延时函数的设置
如果想设计一种具备可重载性的延时函数,查询定时是一个不错而又简单的方法。
这里依旧以定时器3举例:
u32 Delay_count=0;//默认168Mhz,其他情况按照实际修改
void delay_init(void)
{TIM3_int_init(39999,21); //每一us计数器加4,10ms重装一次
} //不能轻易地使用寄存器模式,因为你无法判断会不会被重写//延时nus,
//nus:要延时的us数.
//nus:注意变量类型的限制
void delay_us(u32 nus)
{ u32 last_count=Delay_count;u32 last_cnt=TIM3->CNT;while(((Delay_count-last_count)*40000+TIM3->CNT-last_cnt)<nus*4);
}
这就是实现的方法,很简单,配合上面的10ms定时中断,定时时长可以在一个很大的范围内。这个函数可以重载,因为它的本质是在时间轴上截取一段时间,时间轴一直在延伸,并不会停下,只要超过了预期的时间点,那么它就认为延时已经到达了,注意定时溢出问题在这里没有被考虑。
也许你会注意到它的准确性,这不用担心,CPU在168Mhz下,这个的误差比延时级别us要小很多。
4.IO复用
Stm32F407的IO口非常多,设置起来主要依靠MODER、OTYPER、OSPEEDR、PUPDR,涉及上下拉电阻,输出速度,输出类型等。具有非常强大的复用功能选择。
值得注意的是,Stm32F407有的管脚是具备5v容忍的,如果不具备该特性,最好管脚不要超过3.3V电压。
在输入情况下,速度配置和输出模式设置无所谓,即参数无效。
输出设置推荐使用推挽模式,但负载能力有限,简易外部设计MOS带驱动。
复用功能下,同时兼顾输出和输入,可以对输入寄存器读访问获取状态,这是最常见的情况。
还有一种是模拟配置,用于ADC采集和DAC输出,这个时候也是兼顾输出和输入的:
直接上代码:
GPIOx->MODER&=~(3<<(pinpos*2)); //先清除原来的设置
GPIOx->MODER|=MODE<<(pinpos*2); //设置新的模式
//MODER寄存器用来设置端口模式
//32位寄存器分成16份,分别代表16个Io中的一个,比如A端口,有16个IO
//例如A8,对应第8个io,pinpos=8,则第16位和第17位表示A8的配置位,
//注意有A0无A16,即有第0,1,2....14,15个IO
//OSPEEDR和PUPDR配置位分配情况也一样。
//11=模拟模式,10=复用模式,01=输出模式,00,输入模式(复位状态)
if((MODE==0X01)||(MODE==0X02)) //如果是输出模式/复用功能模式
{ GPIOx->OSPEEDR&=~(3<<(pinpos*2)); //清除原来的设置GPIOx->OSPEEDR|=(OSPEED<<(pinpos*2));//设置新的速度值 GPIOx->OTYPER&=~(1<<pinpos) ; //清除原来的设置GPIOx->OTYPER|=OTYPE<<pinpos; //设置新的输出模式
}
//Mode就是选择的端口模式,OSPEEDR配置速度
//00=2Mhz,01=25Mhz,10=50Mhz,100Mhz(80Mhz)
//OTYPER配置输出模式,32位低16位有用,依次对应端口0-15
//0=推挽(复位默认),1=开漏
//这里对输入模式(包括模拟输入)没有配置,是因为这些对于输入来说无关。
GPIOx->PUPDR&=~(3<<(pinpos*2)); //先清除原来的设置
GPIOx->PUPDR|=PUPD<<(pinpos*2); //设置新的上下拉
//配置上下拉电阻,看具体情况,上下拉具体解释自行百度。GPIOx->AFR[BITx>>3]&=~(0X0F<<((BITx&0X07)*4));//清除相应位的选择
GPIOx->AFR[BITx>>3]|=(u32)AFx<<((BITx&0X07)*4);//开启相应位的选择
//复用寄存器有两个,共64位,依次每4位配置一个端口,共16个IO端口。4位有16种情况,对应16种复用值,具体要看手册。
//这里的BITx是端口的序号,BITX>>3,是获取处于那个寄存器,例如BITX=9,就说明处于第二个AFR配置寄存器中,
//所以移位后为1,表示数组第二项,其他类推,AFX是配置的复用功能选择,根据手册决定。
GPIOx表示端口的地址,比如A端口地址GPIOA,这是在官方头文件里定义好了的。
下面是MODER寄存器的位分配图,其他可看数据手册。
5.串口USART1设置
stm32F407具有多个串口,这是只考虑通过USB转TTL与电脑直接通信的串口。
这里的USART只使用全双工异步通信模式,但它支持LIN模式、IrDA、智能卡、单线半双工等模式。
采样模式支持16倍过采样和8倍过采样,停止位支持1或2位,支持奇偶校验,数据长度支持7和8位,支持DMA通信,具有丰富的标志量和中断源(具体看手册)。
本文使用奇偶校验、DMA、16倍过采样,以及空闲中断。一些错误中断也可以被设置,用以判断是否需要重发数据。
void UART_init(u32 pclk2,u32 bound)
{ float temp;u16 mantissa;u16 fraction; temp=(float)(pclk2*1000000)/(bound*16);//这里默认采取了16倍过采样。PCLK默认单位是Mhz//采用8倍采样计算公式会不同,具体看手册。//波特率的计算公式每种单片机都可能不同,这里是16倍过采样的设置//适合Stm32F407,具体公式的解释看手册,上面非常清晰。mantissa=temp; //得到整数部分fraction=(temp-mantissa)*16; //得到小数部分mantissa<<=4;mantissa+=fraction; RCC->AHB1ENR|=1<<0; //使能PORTA口时钟 RCC->APB2ENR|=1<<4; //使能串口1时钟 //这里使用的是PA端口,使用需要开启对应端口的时钟以及外设的时钟,必不可少,复位时会关闭所有外设时钟
GPIO_Set(GPIOA,PIN9|PIN10,GPIO_MODE_AF,GPIO_OTYPE_PP,GPIO_SPEED_50M,GPIO_PUPD_PU);//PA9,PA10,复用功能,上拉输出,输出速率50M//该单片机直接复用模式即可,除了ADC和输入捕获,其他都可用,且包含输入和输出两块。GPIO_AF_Set(GPIOA,9,7); //PA9,AF7GPIO_AF_Set(GPIOA,10,7);//PA10,AF7 //这里复用功能选择USARTRX和USARTTX//波特率设置USART1->BRR=mantissa; //波特率设置 USART1->CR1&=~(1<<15); //设置OVER8=0,即采用过采样模式 USART1->CR1|=1<<3; //串口发送使能 //使能接收 USART1->CR1|=1<<2; //串口接收使能//这里配置的是DMA串口,所以接收中断去掉了,使用空闲中断表明一次接收传输的结束。USART1->CR3|=1<<7; //串口DMA发送使能USART1->CR3|=1<<6; //串口DMA接收使能USART1->CR1|=1<<4; //串口接收空闲中断使能//下面是奇偶校验USART1->CR1|=1<<12; //一次发11个位,1位起始,9位数据,1位停止USART1->CR1|=1<<10; //开启奇偶校验USART1->CR1|=1<<9; //奇校验,第九位使得数据位1个个数为偶数USART1->CR1|=1<<8; //奇偶校验中断使能//这里开启了两个种中断MY_NVIC_Init(4,0,USART1_IRQn,4);//组4,4级优先级 //如果上面的内容没有编译,则无法接收内容。USART1->CR1|=1<<13; //串口使能
}
USART寄存器不多,主要三个CR配置寄存器,SR状态寄存器,DR数据寄存器,BRR波特率寄存器。
串口收发配置非常简单,所以没有过多注释,具体位介绍看手册。下面是配置思路。
(1)使能端口时钟和USART时钟,配置管脚模式和复用功能
(2)计算出波特率,赋值给BRR寄存器
(3)配置采样模式、数据位、停止位等特性,注意,复位情况下默认是16倍过采样、8位数据位,1位停止位,无奇偶校验等额外功能。所以除非有需求,这些都不用配置,默认的就是最常用的情况。
(4)开启接收和发送,配置需要中断,使能串口。
这里可以看到,没有提供中断服务函数,这将在下面的DMA提供,值得一提的是,奇偶校验是不影响DMA的,该传输的数据一样传,数据位9位,但第九位是校验位,有效数据始终是8位,如果只设置8位数据位,则有效数据是7位。
6.DMA设置
DMA,直接存储器访问,可以无需CPU操作快速大量移动数据。
STM32的DMA支持存储器到外设访问,这用来写USART—DR数据,外设到存储器,则用来读DR寄存器。
DMA操作可以自动清除SR里面的TC、TXE、RXNE标识,并往DR里面读写数据,无需人为干预。
具体的外设介绍看手册,使用思路如下:
(1)在DMA控制器中写入内存区地址,该地址存了你要发送的一系列数据。
(2)DMA-NDTR寄存器中写入要发送的数据项总量,该数据无法在运行中修改,只能读取。
(3)激活DMA。
很简单,具体的寄存器配置只是一次性完成的,使用过程中并不需要频繁配置。
下面是DMA具体配置:
//DMA专用于UART1的发送接收
//双缓冲似乎更加有力,但这里还是采取类似协议的方法,构建发送头。
//使用DMA2的stream5-RX和stream7-TX
//数据宽度8位,增量模式
void DMA_for_UART_init(void)
{ RCC->AHB1ENR|=1<<22;//DMA2时钟使能 while(DMA2_Stream5->CR&0X01||DMA2_Stream7->CR&0X01);//等待DMA可配置 必须En位=0,即不传输的时候才可以写入。DMA2->HIFCR |=61<<6;//清空Stream5所有中断标识DMA2->HIFCR |=61<<22;//清空Stream7所有中断标识//下面是Stream5配置DMA2_Stream5->PAR=(u32)&(USART1->DR); //DMA外设地址DMA2_Stream5->M0AR=(u32)UART_receive_buffer; //DMA 存储器0地址DMA2_Stream5->NDTR=0; //DMA 传输的数据项数,为0暂不传输DMA2_Stream5->CR=0; //先全部复位CR寄存器值 //开启完成中断和错误中断(直接模式错误中断在存储器地址递增情况下无用)//DMA选为流控制器,不使用FIFO模式,即直接模式//DMA2_Stream5->CR|=1<<4; //传输完成中断使能DMA2_Stream5->CR|=1<<2; //传输错误中断使能DMA2_Stream5->CR|=0<<6; //数据传输方向,外设到存储器模式DMA2_Stream5->CR|=0<<8; //非循环模式(即使用普通模式)DMA2_Stream5->CR|=0<<9; //外设非增量模式,外设地址固定DMA2_Stream5->CR|=1<<10; //存储器增量模式,地址增加值为Msize=1字节DMA2_Stream5->CR|=0<<11; //外设数据长度:8位DMA2_Stream5->CR|=0<<13; //存储器数据长度:8位//UART是8位对齐数据DMA2_Stream5->CR|=3<<16; //最高优先级,因为接收及时非常重要//CR->CT为0,CR->DBM=0,默认DMA-SxMoAR,因为没有使用双缓冲模式DMA2_Stream5->CR|=0<<21; //外设突发单次传输DMA2_Stream5->CR|=0<<23; //存储器突发单次传输//直接模式下,上面强制为0DMA2_Stream5->CR|=4<<25; //UART1_RX来自通道4//下面是Stream7配置DMA2_Stream7->PAR=(u32)&(USART1->DR); //DMA外设地址DMA2_Stream7->M0AR=(u32)UART_send_buffer; //DMA 存储器0地址DMA2_Stream7->NDTR=0; //DMA 传输的数据项数,为0暂不传输DMA2_Stream7->CR=0; //先全部复位CR寄存器值 //开启完成中断和错误中断(直接模式错误中断在存储器地址递增情况下无用)//DMA选为流控制器,不使用FIFO模式,即直接模式DMA2_Stream7->CR|=1<<4; //传输完成中断使能DMA2_Stream7->CR|=1<<2; //传输错误中断使能DMA2_Stream7->CR|=1<<6; //数据传输方向,存储器到外设模式DMA2_Stream7->CR|=0<<8; //非循环模式(即使用普通模式)DMA2_Stream7->CR|=0<<9; //外设非增量模式,外设地址固定DMA2_Stream7->CR|=1<<10; //存储器增量模式,地址增加值为Msize=1字节DMA2_Stream7->CR|=0<<11; //外设数据长度:8位DMA2_Stream7->CR|=0<<13; //存储器数据长度:8位//UART是8位对齐数据DMA2_Stream7->CR|=1<<16; //中优先级,发送就随便啦//CR->CT为0,CR->DBM=0,默认DMA-SxMoAR,因为没有使用双缓冲模式DMA2_Stream7->CR|=0<<21; //外设突发单次传输DMA2_Stream7->CR|=0<<23; //存储器突发单次传输//直接模式下,上面强制为0DMA2_Stream7->CR|=4<<25; //UART1_TX来自通道4//初始化配置完成了,开启和数据项是根据实际情况配置的。MY_NVIC_Init(4,0,DMA2_Stream5_IRQn,4);//组4,4级优先级MY_NVIC_Init(4,0,DMA2_Stream7_IRQn,4);//组4,4级优先级
}
DMA有两个,每个有7个流(Stream),使用哪个DMA和哪个流是根据手册决定的:
必须根据对应关系使用,这里使用的USART1RX和USART1TX,它们同属于DMA2,所以存在总线竞争,故把接收优先级设高,避免接收数据覆盖。注意,同一个DMA的7个流是用一个总线的,是不能并发操作的。
这里是使用直接模式,没有使用FIFO,FIFO更复杂,但也更强大,支持突发传输和数据格式不对齐的情况,但本文不考虑。
串口收发数据都是8位,即一个字节,这在USART里面已经设置好了。
串口的DR寄存器是固定的,所以不需要随着读写数据而改变地址,但内存中的数据存储区则不一样,设置为递增模式,则传一个字节就移到下一个字节处,注意,此时M0AR中的地址是没有改变的。
DMA接收完成是由USART接收空闲中断来判断,所以DMA的接收完成中断是单纯用来记录是否出现溢出等错误的。
DMA发送中断是判断发送是否完成的标识。
注意,这里DMA中断都配置成了组4,在同一个项目中,中断的分组应该是相同的,只有这样,才会按照预设的逻辑执行。
下面是具体代码设计,u8,u16,u32代表无符号整数,应根据硬件情况确定。
#define USART_RECEIVE_LENGTH 32 //定义单个缓冲区最大接收字节数
#define USART_SEND_LENGTH 1024 //定义单个缓冲区最大发送字节数#define SEND_BUFFER_NUMBER 12 //发送缓冲区数
#define RECEIVE_BUFFER_NUMBER 32 //10ms内最大接收指令数typedef struct
{u8 flag;//标志量u16 amount;//数据项量u8 buffer;//缓冲区选择u8 ready[RECEIVE_BUFFER_NUMBER];//判断对应缓冲区是否就绪u8 *address[RECEIVE_BUFFER_NUMBER];u8 *data_info;//数据帧的信息已缓存地址
}_DMA_info;
typedef struct
{u8 flag[RECEIVE_BUFFER_NUMBER];u8 length[RECEIVE_BUFFER_NUMBER];
}_DMA_deal;
typedef struct
{void (*init)(u32 bound);//初始化串口void (*send_deal)();//需要调用的函数_DMA_info send;//发送信息_DMA_info receive;//接收信息_DMA_deal deal;//接收的信息处理情况
}_DMA_UART;u8 UART_receive_buffer[RECEIVE_BUFFER_NUMBER][USART_RECEIVE_LENGTH];
//接收缓冲,最大USART_RECEIVE_LENGTH个字节
u8 UART_send_buffer[SEND_BUFFER_NUMBER][USART_SEND_LENGTH];//发送缓存。_DMA_UART TX=
{TX_init,DMA_UART_deal,//这两个是函数,将在后面实现{0,1,0,{0},{NULL},NULL,NULL,NULL},{1,1,0,{0},{NULL},NULL,NULL,NULL},{33},//开头密匙为3{0},
};
上面是具体一些数据框架,UART_receive_buffer、UART_send_buffer这两个数据缓冲区分别用来缓存接收和发送数据,定义成二维数组格式,可以形成多缓冲区通信,数据传输持续进行。
_DMA_UART结构体用来对DMA串口操作,事实上,内部已经实现了自动操作,该结构体仅仅用来展示当前DMA信息(如哪些缓冲区有数据未处理等等)
这里把printf函数重定向到输出于UART_receive_buffer中,
int fputc(int ch, FILE *f)
{ *(TX.send.data_info) = (u8) ch;TX.send.data_info++; return ch;
}
这里只展示关键代码,Printf自定义代码网上很多。这里实际上就是指针赋值然后自增,由于二维数组是连续的,实际上写过了单个缓冲区问题不大,但终究存在写溢出问题,本文不考虑指针保护问题,虽然加一个指针判断代码就可以解决,但是这样将增加“非常多”额外的开销,如果 每次都判断一次的话。
void TX_init(u32 bound)
{u8 temp=0;UART_init(84,bound);DMA_for_UART_init();TIM3_int_init(999,839);//10ms处理一次,这个在前面文章里的延时函数中已经初始化一次了for(temp=0;temp<SEND_BUFFER_NUMBER;temp++){TX.send.address[temp]=UART_send_buffer[temp];}for(temp=0;temp<RECEIVE_BUFFER_NUMBER;temp++){TX.receive.address[temp]=UART_receive_buffer[temp];}TX.receive.order_info=UART_receive_buffer[0];TX.receive.data_info=UART_receive_buffer[0];TX.receive.file_info=UART_receive_buffer[0];TX.send.order_info=UART_send_buffer[0];TX.send.data_info=UART_send_buffer[0];TX.send.file_info=UART_send_buffer[0];TX.receive.amount=USART_RECEIVE_LENGTH;DMA_Receive_Info();//默认地址都是第一个缓冲区
}
这是DMA串口的初始化代码,初始化三个外设,定时器是用来自动操作的,即每隔10秒处理一次,在961200波特率下,10ms最多发800+字符,速率有限,在115200及以下波特率下,能写的更少,10ms是一个不错的时间。后面是初始化结构体,把接收缓冲区记录下来。
void DMA2_Stream5_IRQHandler(void)//接收DMA中断
{if(DMA2->HISR&(1<<11))//传输完成中断{//发生错误,此时可能USART接收溢出了}else if(DMA2->HISR&(1<<9))//传输错误中断{//DMA传输错误了}DMA2->HIFCR |=61<<6;//清空Stream5所有中断标识
}void DMA2_Stream7_IRQHandler(void)//发送DMA中断
{if(DMA2->HISR&(1<<27))//传输完成中断{ TX.send.ready[TX.send.buffer]=1;//发送完成,标记当前缓冲区完成//这里可以继续判断是否还有需要发送的缓冲区}else if(DMA2->HISR&(1<<25))//传输错误中断{//DMA传输错误}DMA2->HIFCR |=61<<22;//清空Stream7所有中断标识
}
void USART1_IRQHandler(void)
{u8 temp;if(USART1->SR&(1<<4))//空闲线路{temp =USART1->DR;TX.deal.length[TX.receive.buffer]=TX.receive.amount-(u8)DMA2_Stream5->NDTR;//获取接收到的字符总数TX.deal.flag[TX.receive.buffer]=DEAL_NEED;TX.receive.ready[TX.receive.buffer]=1;//已就绪for(temp=0;temp<RECEIVE_BUFFER_NUMBER;temp++){ if(TX.deal.flag[temp]==DEAL_DONE){TX.receive.buffer=temp;//选取缓冲区break;}}TX.deal.flag[TX.receive.buffer]=DEAL_DONE;//这里先强制重写DMA_Receive_Info();//确定下一次接收参数}else if(USART1->SR&(1<<0))//奇偶检验错误中断{temp =USART1->DR;//此时发生了校验错误}
}
这是DMA和USART的中断,中断很多是空的,因为这些是错误中断,根据具体情况使用。要注意USART的中断标识清除要通过软件序列,即先读SR再读写DR,直接无法清零。USART空闲中断在接收一条命令后,就会进入中断,这是DMA会将缓冲区移到下一个,一般来说,串口接收的命令长度短,但数量多,所以RECEIVE_BUFFER_NUMBER可以设置大些,而USART_RECEIVE_LENGTH设置短些,这样总体大小维持稳定。
DMA单次接收的数据量设置得比USART_RECEIVE_LENGTH大,一般来说不会传输完成,而是手动传输暂停,此时已传输的数据项数可通过 DMA2_Stream5->NDTR得出,它减少的数量就是已传输的数量。
void DMA_Send_Info(void)
{u32 amount=(u32)TX.send.data_info-(u32)UART_send_buffer[TX.send.buffer];if(amount>0){TX.send.data_info=UART_send_buffer[(TX.send.buffer+1)%SEND_BUFFER_NUMBER];if(amount>USART_SEND_LENGTH)//发送区已满{TX.send.amount=USART_SEND_LENGTH;//此时发生错误,传输太慢了,写的太多}elseTX.send.amount=(u16)amount;DMA2_Stream7->CR&=~(1<<0); //关闭DMA传输 while(DMA2_Stream7->CR&0X1); //确保DMA可以被设置DMA2_Stream7->M0AR=(u32)TX.send.address[TX.send.buffer]; //DMA 存储器0地址 DMA2_Stream7->NDTR=TX.send.amount;//接收数据量DMA2_Stream7->CR|=1<<0; //开启DMA传输TX.send.buffer=(TX.send.buffer+1)%SEND_BUFFER_NUMBER;}else{ //此时没有数据可发}
}//设置DMA接收区的参数,默认情况下每次开启64个接收,不超过64个的字符可以被很好接收
//缓存区软件自动循环,无需人为干扰。
//也不考虑信息的具体处理,只是给出一定的缓存时间用处理。
void DMA_Receive_Info(void)
{DMA2_Stream5->CR&=~(1<<0); //关闭DMA传输 while(DMA2_Stream5->CR&0X1); //确保DMA可以被设置DMA2_Stream5->M0AR=(u32)TX.receive.address[TX.receive.buffer]; //DMA 存储器0地址 DMA2_Stream5->NDTR=TX.receive.amount;//接收数据量DMA2->HIFCR |=61<<6;//清空Stream5所有中断标识DMA2_Stream5->CR|=1<<0; //开启DMA传输TX.receive.ready[TX.receive.buffer]=0;//表示新的数据还没有准备好
}
这是关键的收发设置,对于发送,直接用指针计算需要发送的数据量,如果超过缓冲区长度,则会限制数量,因为10ms内能发送的数据量是有限的,速率是一定的,1000字节的缓冲区10ms在921600波特率下,是发不完的。
计算完数据项数量,立刻把缓冲区的写指针移向下一个缓冲区,使接下来的写操作不会影响数据发送。
接收设置要清零中断标识,因为手动关闭也会触发中断标识,但此时没有进入中断来清除。
可以看到,当前缓冲区是存在TX.xxx.buffer中的,在必要的地方计算出新值,而其他位置,只用访问其值就可以了,它是自动完成计算的,无需操心。
void TIM3_IRQHandler(void)
{ if(TIM3->SR&0X0001)//溢出中断//该寄存器包含捕捉、比较、触发、更新及重复中断状态标志位。//这里读取更新中断标志,下溢或上溢时会产生(CR1_UDIS应为0)。{Delay_count++;//10ms加一次,总计时为2400万秒DMA_Send_Info();} TIM3->SR&=~(1<<0);//清除中断标志位
}
把DMA_Send_Info()放在这里,就可以一直触发下去了。可以发现,这只是单纯的收发,没有数据处理在里面,它们分离了,对于收到的数据,在其他程序里处理,DMA接收程序已经打上了必要的标签。发送只管对着TX.send.data_info写就行,DMA发送程序会自动移动指针使其位于合适的缓冲区。
经测试,双向传输最大发送速率可达理论速率1/3左右(每一次发送700-800字节需要收到4字节的指令),即每秒30000+字节,问题存在于发送的不及时,以及接收发送的总线竞争,这些问题有待解决。
(未完待续)