|
本帖最后由 xiaojie0513 于 2018-9-18 21:07 编辑 4 t( \2 l5 { x# F/ y/ L1 W 引言 在使用stm32或者其他单片机的时候,会经常使用到串口通讯,那么如何有效地接收数据呢?假如这段数据是不定长的有如何高效接收呢? DMA简介 什么是DMA DMA:全称Direct Memory Access,即直接存储器访问 DMA 传输将数据从一个地址空间复制到另外一个地址空间。CPU只需初始化DMA即可,传输动作本身是由 DMA 控制器来实现和完成。典型的例子就是移动一个外部内存的区块到芯片内部更快的内存区。这样的操作并没有让处理器参与处理,CPU可以干其他事情,当DMA传输完成的时候产生一个中断,告诉CPU我已经完成了,然后CPU知道了就可以去处理数据了,这样子提高了CPU的利用率,因为CPU是大脑,主要做数据运算的工作,而不是去搬运数据。DMA 传输对于高效能嵌入式系统算法和网络是很重要的。 在STM32的DMA资源STM32F1系列的MCU有两个DMA控制器(DMA2只存在于大容量产品中),DMA1有7个通道,DMA2有5个通道,每个通道专门用来管理来自于一个或者多个外设对存储器的访问请求。还有一个仲裁器来协调各个DMA请求的优先权。 而STM32F4/F7/H7系列的MCU有两个DMA控制器总共有16个数据流(每个DMA控制器8个),每一个DMA控制器都用于管理一个或多个外设的存储器访问请求。每个数据流总共可以有多达8个通道(或称请求)。每个通道都有一个仲裁器,用于处理 DMA 请求间的优先级。6 n+ @+ z: `% Z* n. Z DMA接收数据 DMA在接收数据的时候,串口接收DMA在初始化的时候就处于开启状态,一直等待数据的到来,在软件上无需做任何事情,只要在初始化配置的时候设置好配置就可以了。等到接收到数据的时候,告诉CPU去处理即可。
其实,有很多方法:
DMA+串口空闲中断 这两个资源配合,简直就是天衣无缝啊,无论接收什么不定长的数据,管你数据有多少,来一个我就收一个,就像广东人吃“山竹”,来一个吃一个~(最近风好大,我好怕)。 可能很多人在学习stm32的时候,都不知道idle是啥东西,先看看stm32串口的状态寄存器: 当我们检测到触发了串口总线空闲中断的时候,我们就知道这一波数据传输完成了,然后我们就能得到这些数据,去进行处理即可。这种方法是最简单的,根本不需要我们做多的处理,只需要配置好,串口就等着数据的到来,dma也是处于工作状态的,来一个数据就自动搬运一个数据。 接收完数据时处理串口接收完数据是要处理的,那么处理的步骤是怎么样呢?
注意事项 STM32的IDLE的中断在串口无数据接收的情况下,是不会一直产生的,产生的条件是这样的,当清除IDLE标志位后,必须有接收到第一个数据后,才开始触发,一断接收的数据断流,没有接收到数据,即产生IDLE中断。如果中断发送数据帧的速率很快,MCU来不及处理此次接收到的数据,中断又发来数据的话,这里不能开启,否则数据会被覆盖。有两种方式解决:
实验效果: 当外部给单片机发送数 据的时候,假设这帧数据长度是1000个字节,那么在单片机接收到一个字节的时候并不会产生串口中断,只是DMA在背后默默地把数据搬运到你指定的缓冲区里面。当整帧数据发送完毕之后串口才会产生一次中断,此时可以利用DMA_GetCurrDataCounter()函数计算出本次的数据接受长度,从而进行数据处理。 串口的配置 /**9 f1 e3 ?6 ]0 V3 X很简单,基本与使用串口的时候一致,只不过一般我们是打开接收缓冲区非空中断,而现在是打开空闲中断——USART_ITConfig(DEBUG_USARTx, USART_IT_IDLE, ENABLE);。 * @brief USART GPIO 配置,工作参数配置 * @param 无 * @retval 无 */ void USART_Config(void) {4 u+ l7 M9 ~$ V/ h# t/ O/ a5 r GPIO_InitTypeDef GPIO_InitStructure;( d4 t2 G3 H! k) S8 \ USART_InitTypeDef USART_InitStructure;, r- [6 G' g6 u# O // 打开串口GPIO的时钟( J }, u+ i* y. X DEBUG_USART_GPIO_APBxClkCmd(DEBUG_USART_GPIO_CLK, ENABLE);) i. w S' u5 K8 I: V- ? 6 q9 ~- B/ p9 T) D# l+ m* ` V# ` // 打开串口外设的时钟2 T4 {1 \. d; X+ P* u DEBUG_USART_APBxClkCmd(DEBUG_USART_CLK, ENABLE); & W! M( c3 ~* v- N k1 A' C, N // 将USART Tx的GPIO配置为推挽复用模式 GPIO_InitStructure.GPIO_Pin = DEBUG_USART_TX_GPIO_PIN; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;9 R- G/ W3 D% B' b$ m Z GPIO_Init(DEBUG_USART_TX_GPIO_PORT, &GPIO_InitStructure);% ~: w6 Y# n( C3 e' j. U4 g * w) Y9 S6 k/ {5 c" V: R // 将USART Rx的GPIO配置为浮空输入模式 GPIO_InitStructure.GPIO_Pin = DEBUG_USART_RX_GPIO_PIN;4 [5 \" T) b8 V, ?0 |9 d: D GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(DEBUG_USART_RX_GPIO_PORT, &GPIO_InitStructure); // 配置串口的工作参数% l8 _9 b9 t% w9 ~: V // 配置波特率+ a6 J* G5 J8 j USART_InitStructure.USART_BaudRate = DEBUG_USART_BAUDRATE;4 r' j Y) D6 j // 配置 针数据字长 USART_InitStructure.USART_WordLength = USART_WordLength_8b;8 ~6 U: A, ~4 Z5 c/ y // 配置停止位3 N) Y: x) V& e" `) w6 s USART_InitStructure.USART_StopBits = USART_StopBits_1;6 I% m2 X3 e, s" z+ p // 配置校验位: i& d! i6 Q; k- t. x USART_InitStructure.USART_Parity = USART_Parity_No ; // 配置硬件流控制 USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; // 配置工作模式,收发一起 USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;3 {' {% N S) g: {2 H8 s // 完成串口的初始化配置 USART_Init(DEBUG_USARTx, &USART_InitStructure); // 串口中断优先级配置& D0 E- k Q3 N9 M0 K# J) B0 v NVIC_Configuration(); B& ^9 W4 y6 Q4 Q #if USE_USART_DMA_RX // 开启 串口空闲IDEL 中断7 S' H& w9 O. ~ USART_ITConfig(DEBUG_USARTx, USART_IT_IDLE, ENABLE); ! ^! U3 i0 n5 s' [( ?) r2 m // 开启串口DMA接收 USART_DMACmd(DEBUG_USARTx, USART_DMAReq_Rx, ENABLE); /* 使能串口DMA */ USARTx_DMA_Rx_Config();3 A6 } ~1 T) V: l2 H! m% J #else // 使能串口接收中断 USART_ITConfig(DEBUG_USARTx, USART_IT_RXNE, ENABLE); #endif$ _, [: Y8 K( g, k #if USE_USART_DMA_TX 0 _0 y( p* `! I0 E$ H4 x, v // 开启串口DMA发送 // USART_DMACmd(DEBUG_USARTx, USART_DMAReq_Tx, ENABLE); ?7 X2 k: H! g. t; } USARTx_DMA_Tx_Config();9 ~9 ?& A' x! v0 f4 r" ?' D1 c. q #endif$ z* D# o6 t8 R# \2 N( ` // 使能串口) Y* S; O4 U# {+ c. g USART_Cmd(DEBUG_USARTx, ENABLE); 9 o: q$ B4 e4 \9 ` } 串口DMA配置 把DMA配置完成,就可以直接打开DMA了,让它处于工作状态,当有数据的时候就能直接搬运了。 #if USE_USART_DMA_RX 3 W% i' Q- b2 C/ T# h R& v8 `# h1 k& k; S2 w* { static void USARTx_DMA_Rx_Config(void); C& S3 r; p1 ~3 G, u {. q7 K# r8 Q2 v DMA_InitTypeDef DMA_InitStructure; * P. O& _0 t8 ^7 }3 K; p. O // 开启DMA时钟 RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);7 b* e- T- K" S& ^ // 设置DMA源地址:串口数据寄存器地址*/& H8 v1 B- s- Z. M2 ^7 [# t# d DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)USART_DR_ADDRESS;3 G) Y9 @; q, X: [, }" {* ` // 内存地址(要传输的变量的指针)1 d8 r* ^+ q7 P3 G6 O+ N E, D DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)Usart_Rx_Buf;& X# s5 h$ `& s5 P9 [$ v9 D7 K9 A5 c // 方向:从内存到外设 DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; // 传输大小 DMA_InitStructure.DMA_BufferSize = USART_RX_BUFF_SIZE; // 外设地址不增 DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;- L- P Y7 w. y0 T1 n* ] // 内存地址自增1 i* o+ Y# F, F* l+ O DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;/ d7 D3 ?; V- M3 [1 S // 外设数据单位 DMA_InitStructure.DMA_PeripheralDataSize = 6 b0 W0 e5 A( F: n/ R5 p DMA_PeripheralDataSize_Byte;$ ]$ h! F% h+ _+ u2 z // 内存数据单位 DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; // DMA模式,一次或者循环模式 //DMA_InitStructure.DMA_Mode = DMA_Mode_Normal ;/ Q; ]3 F4 N' ?* M7 A' J DMA_InitStructure.DMA_Mode = DMA_Mode_Circular; $ _$ N" M. M$ N0 U1 \6 ~' l6 ^$ Z // 优先级:中 DMA_InitStructure.DMA_Priority = DMA_Priority_VeryHigh; * p0 z* B2 X& g5 i7 ^ // 禁止内存到内存的传输 C3 X& i) B* X. D DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;0 q8 k; F7 I1 F2 T% K // 配置DMA通道 % C; `7 } v3 n DMA_Init(USART_RX_DMA_CHANNEL, &DMA_InitStructure); // 清除DMA所有标志* L5 ?* D/ s. v; ] DMA_ClearFlag(DMA1_FLAG_TC5); DMA_ITConfig(USART_RX_DMA_CHANNEL, DMA_IT_TE, ENABLE); // 使能DMA DMA_Cmd (USART_RX_DMA_CHANNEL,ENABLE);. ]+ P% [3 d% X# B+ { }" g6 E% b" P( Z/ { #endif# a X( i; o3 g# q* o 接收完数据处理 因为接收完数据之后,会产生一个idle中断,也就是空闲中断,那么我们就可以在中断服务函数中知道已经接收完了,就可以处理数据了,但是中断服务函数的上下文环境是中断,所以,尽量是快进快出,一般在中断中将一些标志置位,供前台查询。在中断中先判断我们的产生在中断的类型是不是idle中断,如果是则进行下一步,否则就无需理会。 /********************************************************************; ~/ p1 l8 J4 r0 m5 Q& h * @brief 串口中断服务函数" B7 X0 N' D1 e- d, N; N * @author jiejie * @version V1.02 C8 k r0 @- K& q% R * @date 2018-xx-xx6 [' D6 n+ ]. x$ n* M4 \& o7 f ******************************************************************( b6 m( j! ^9 M& G2 S */ . a8 w3 S. o! P. Y; r void DEBUG_USART_IRQHandler(void)4 B! O% l' b* w$ y& X3 b( s3 j { #if USE_USART_DMA_RX /* 使用串口DMA */ if(USART_GetITStatus(DEBUG_USARTx,USART_IT_IDLE)!=RESET) { /* 接收数据 */9 B* h$ r" g0 K Receive_DataPack();) y. f: m- F) F! ` // 清除空闲中断标志位 USART_ReceiveData( DEBUG_USARTx ); b- q8 V# q6 z: A$ l } " N& J1 ~5 Q0 |' R4 S# ^7 C2 D, v { #else5 V Y, s9 V! ]" f- u4 w4 X /* 接收中断 */ if(USART_GetITStatus(DEBUG_USARTx,USART_IT_RXNE)!=RESET) { " J( H$ P& M) T/ n/ G: t- W7 C% G0 N6 O Receive_DataPack(); }. {3 w) \! H: W. ^ #endif( n' H3 g! A: t1 y }/ }4 H5 r } Receive_DataPack() 这个才是真正的接收数据处理函数,为什么我要将这个函数单独封装起来呢?因为这个函数其实是很重要的,因为我的代码兼容普通串口接收与空闲中断,不一样的接收类型其处理也不一样,所以直接封装起来更好,在源码中通过宏定义实现选择接收的方式!更考虑了兼容操作系统的,可能我会在系统中使用dma+空闲中断,所以,供前台查询的信号量就有可能不一样,可能需要修改,我就把它封装起来了。不过无所谓,都是一样的。 /************************************************************* @brief Uart_DMA_Rx_Data! j9 B" C6 M$ V2 s4 R * @param NULL * @return NULL+ |. v0 W! J# Y# t * @author jiejie * @github http://github.com/jiejieTop * @date 2018-xx-xx w7 O; z: e) Y$ a f3 W; Z * @version v1.0 * @note 使用串口 DMA 接收时调用的函数 ***********************************************************/ #if USE_USART_DMA_RX void Receive_DataPack(void)- K# r0 R/ E% g, `6 d5 T { /* 接收的数据长度 */- O; V( B- P& j. i uint32_t buff_length;& B& d7 M0 T& y" [. K8 \ + D* H b/ x+ a" p# o$ h /* 关闭DMA ,防止干扰 */ DMA_Cmd(USART_RX_DMA_CHANNEL, DISABLE); /* 暂时关闭dma,数据尚未处理 */ & {9 i3 ]) H0 a8 o4 }7 \ |, ^ /* 清DMA标志位 */+ ^% d% H# [9 h DMA_ClearFlag( DMA1_FLAG_TC5 ); /* 获取接收到的数据长度 单位为字节*/# e+ L- u7 n+ k+ W1 { L buff_length = USART_RX_BUFF_SIZE - DMA_GetCurrDataCounter(USART_RX_DMA_CHANNEL);) m+ O# b; { s4 e# S /* 获取数据长度 */% K' K" M# X% A/ g! h) `$ W4 W5 y Usart_Rx_Sta = buff_length; PRINT_DEBUG("buff_length = %d\n ",buff_length); /* 重新赋值计数值,必须大于等于最大可能接收到的数据帧数目 */ USART_RX_DMA_CHANNEL->CNDTR = USART_RX_BUFF_SIZE; /* 此处应该在处理完数据再打开,如在 DataPack_Process() 打开*/ DMA_Cmd(USART_RX_DMA_CHANNEL, ENABLE); 5 ]. r6 j7 x/ k+ a: s /* (OS)给出信号 ,发送接收到新数据标志,供前台程序查询 */ /* 标记接收完成,在 DataPack_Handle 处理*/ Usart_Rx_Sta |= 0xC000; 3 P0 d6 `& v3 u: ?6 p5 H% `/ s' }: t /* : o6 n/ |8 `7 Y* S" \$ A7 E9 \- N DMA 开启,等待数据。注意,如果中断发送数据帧的速率很快,MCU来不及处理此次接收到的数据,3 j$ Q4 C: ?5 Z" z$ Z2 ?* g 中断又发来数据的话,这里不能开启,否则数据会被覆盖。有2种方式解决:1 N' c* e1 ]: F" J& m8 x 1. 在重新开启接收DMA通道之前,将Rx_Buf缓冲区里面的数据复制到另外一个数组中,3 S" _1 Y2 ?: a) ]+ m8 s7 { 然后再开启DMA,然后马上处理复制出来的数据。 2. 建立双缓冲,重新配置DMA_MemoryBaseAddr的缓冲区地址,那么下次接收到的数据就会 保存到新的缓冲区中,不至于被覆盖。 */ }# ]" P ]% M- n* C1 M f1使用dma是非常简单的,我在f4用dma的时候也遇到一些问题,最后看手册解决了,打算下一篇文章就写一下调试过程,没有什么是debug不能解决的,如果有,那就两次。今天台风天气,连着舍友的WiFi更新的文章~中国电信还是强,台风天气信号一点都不虚,我的移动卡一动不动-_-. 不设置回复可见,大家一起学习~感谢 本文为杰杰原创如需转载,请说明出处: |
微信公众号
手机版
这篇先前看过,好像是正点原子的例子改的,解析接收数据没有启到环形队列特点;比如当你接收到一帧数据时,(USART_RX_STA|=0x8000; //接收完成了);假设CPU处理外设多,没有及时处理这一帧数据,此时串口又来了一帧数据,就会丢数据了,没有用到环形队列缓存功能,
void USART1_IRQHandler(void) //串口1中断服务程序' Q8 \2 v3 F- `, ~
{
u8 Res;
#if SYSTEM_SUPPORT_OS //如果SYSTEM_SUPPORT_OS为真,则需要支持OS.5 i! ]) o6 V B( y+ n
OSIntEnter();
#endif
if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) //接收中断(接收到的数据必须是0x0d 0x0a结尾)- R/ f! c) \$ j" i- u* m8 o" ~! A
{ t5 g! ~' [4 b( j2 b: k( D7 n
Res =USART_ReceiveData(USART1); //读取接收到的数据+ Y( k8 F7 P, K3 [; ]+ M
) J& ?6 @! t+ p, e, _8 o/ z( j: j8 V
if((USART_RX_STA&0x8000)==0)//接收未完成0 U7 ~$ |' p7 Z
{
if(USART_RX_STA&0x4000)//接收到了0x0d& X. [0 R# \" P) ]) T T5 e
{
if(Res!=0x0a)USART_RX_STA=0;//接收错误,重新开始+ C3 R. U4 l3 F8 t% O0 R
else USART_RX_STA|=0x8000; //接收完成了 9 ]3 m0 _8 m$ y6 [" n
}/ }. R) n; v6 A! ]3 x
else //还没收到0X0D* ~# c6 C. {" l: E& \/ i4 ~
{
if(Res==0x0d)USART_RX_STA|=0x4000;, U5 N# G8 y4 k
else
{, E- [$ R( n5 l! d
USART_RX_BUF[USART_RX_STA&0X3FFF]=Res ;( y0 `, I6 P" o/ N8 `' l
USART_RX_STA++;
if(USART_RX_STA>(USART_REC_LEN-1))USART_RX_STA=0;//接收数据错误,重新开始接收
}
}) j# U! t# ?( Q( Q" O2 `
}
} ]/ ~ o$ F& H4 C7 q& l8 I
#if SYSTEM_SUPPORT_OS //如果SYSTEM_SUPPORT_OS为真,则需要支持OS.2 L& L# D+ W* H1 H( t
OSIntExit(); % r6 M: A6 f; `) g7 ~
#endif
}
这个逻辑好像不对,
1、接收数据就是直接放在环形队列中,待CPU有空闲时根据环形队列队头与队尾位置,逐步取出接收到的数据进行处理;
2、如果你每接收到一个数据都去中断判断是否接收一帧数据,会影响CPU的效率,接收中断只接收数据,不做任何数据分析,来一个存一个,减少在中断里处理的时间;待CPU空闲时去环形队列中取数据、解析数据
3、已经定义了一个环形队列,还去定义一个缓冲区好像有点多余,如果CPU没有时间处理那就不去环形队列取数据即可了,空闲时去取不就可以了。3 ^1 T& J) b* N% E$ M Z. l1 ^
这里说的是你发的“STM32进阶之串口环形缓冲区实现”贴子,对于贴子内容是采用环形串口收接,没有说这篇贴子DMA处理;当然有些单片机没有DMA处理,采用环形接收的数据,看到“STM32进阶之串口环形缓冲区实现”这篇贴子,说了一下个人的理解,: D, y$ B" \2 M
对于这种环形队列加中断的方式接收数据和楼主探讨如何更高效接收与解析数据的方法。8 t/ D' G* ~; }
有
STM32进阶之串口环形缓冲区实现# _- s' m3 B& K
https://www.stmcu.org.cn/module/ ... amp;fromuid=3250941
(出处: 意法半导体STM32/STM8技术社区)+ O5 d9 o* ~/ ]% K0 J" X; R
% ]! ^/ R3 B; b' @6 e' {
你这是接收,跟缓冲区有啥关系啊?
你接收完如果暂时没法处理可以将数据放到缓冲区去,然后有空再去处理,,,你这仅仅是接收而已,你在哪放入缓冲区呢
我看到我们一同事在用这种代码格式,我想他也写不出这样的格式呀,原来是出自正点原子
f1的dma是没有FIFO的吧,dma接收根本无需CPU去处理的好吗,先理解一下吧,,,,,dma接收的数据是放到指定的缓冲区的,并没有使用环形队列,,,,使用环形队列需要CPU的参与,,这是f1的,你可以在接收完重置dma存放数据的位置,也能起到缓存的作用. {: b2 O! \3 Y1 _. d) ?
" D, M& W( f" `1 b7 K& o( J
支持FIFO的dma无需这样子做。
每来一个字节的数据就进入中断一次,效率太低,,,
dma接收就可以了,够高效了,
不要断章取义,也不要出口成脏,你说的DMA效率高效,不一定每款单片机都有DMA,同时也不防碍讨论环形队列接收数据高效的方法。
没有出口成章啊,在一般应用中数据流没那么快,所以说高效不高效还得看项目需求。如果每一帧的间隔时间比较长,或者数据交互比较不频繁,我觉得用不用都可以
其实,反正我喜欢dma+空闲中断,,,我一般都用32的芯片,如果非要说环形缓冲区的话,,,,Linux的kfifo很经典,我也移植到32了,,到时候可以发出来看看,支持os