|
前言 对于 STM32 串口的使用,确实很简单使用 STM32CubeMX 做好初始化,就可以直接使用了。 但是最近在某些产品上使用串口同时收发的时候,发现有时候串口会收不到数据了,但是发送正常,而且这个问题再数据量大的时候很容易出现,于是乎进行了好几天的问题测试…… 3 P4 R; E( W C8 c3 q
一、 HAL 库串口收发先简单回顾一下 STM32 HAL库串口收发是如何使用的。
R2 y. y% U1 e1 E$ K5 Z" F! X3 m' p 1.1 串口发送对于 STM32 来说,串口发送有3中方式: - 轮询发送;
- 中断发送;
- DMA发送;+ D; G! b) W0 L! a8 j5 |2 Z0 @
在实际产品上,大部分项目中都用的是 轮询 方式发送,本次出现接收卡死的问题的产品也是采用的轮询发送,所以我简单的说明一下轮询发送,其他两种方式为 STM32 学习的基础问题,这里就不过多讨论。 发送相对简单,在 HAL 库实际都是使用HAL_UART_Transmit函数:
7 d" k( z$ f/ {! ?$ o! H1 [& G
! G! E6 X Q: r2 A5 v5 p( r M# g1 Z4 X# D7 L; [1 R) R
不管是哪个串口发送,都做了个简单的发送函数:
& A" b: p& T$ w2 {" a7 @ v T
; ^0 L" V, R0 H9 p; x! H
; }. T9 z6 N: ~* H/ [* E发送没什么好说的,简单易用。 9 _& g* }2 G$ F# _
1.2 串口接收和串口发送一样,串口接收有3中方式: - 轮询接收;
- 中断接收;
- DMA接收;- g" g8 Y+ q% m1 g6 _
在我们正常的项目使用中,一般都是 中断接收 或者 DMA 接收,基本上不会使用 轮询接收的方式。 那么对于本次出问题的产品,我采用的是 中断接收的方式。 但是相比较发送,在 HAL 库中 使用中断接收的方式就有点 “五花八门” 的感觉。 2 W, R9 q, q# ^9 ?1 n
1.2.1 标准库接收在标准库的时候,我们经常这么用,在串口初始化的时候使用下面的语句使能中断: - //在串口初始化代码之后加上中断使能
$ z2 X7 M% \5 ^2 }( s. r- |0 E) Y - USART_ITConfig(USART3,USART_IT_RXNE,ENABLE);
复制代码 ; `% O1 X7 [ l3 M5 \9 t% z
如果需要自动判断一帧数据,我们再开启一下 IDLE 中断: - USART_ITConfig(USART3, USART_IT_IDLE, ENABLE);
复制代码
, |* ~2 n- e1 }7 n2 G然后在对应的中断处理函数中,直接读取 DR 寄存器(STM32F103 而言为 DR,STM32L051 为RDR),然后当 IDLE 中断产生,也可以处理一下标志位: - //自己用的,用到了 USART_IT_IDLE 标志位,有时候不合适
+ U3 a' [. V, ?( F! S6 R T - void USART3_IRQHandler(void) //串口3中断服务程序/ \5 i8 ~2 t# I {' j/ r3 [9 g
- {4 T# g( J3 Z$ _5 F/ e# \" T
- u8 clear3=clear3; //消除编译器没有用到的提醒
( t+ O: z+ v; L: G - // u8 Res;
9 K1 {( Q: b8 o( h0 ~+ B
5 _! S# @) n* G- if(USART_GetITStatus(USART3, USART_IT_RXNE) != RESET) 1 [4 Z. @* {1 O
- {
. q. e% F/ _! J: o0 S, ?8 d - USART_Enocean_BUF[Enocean_Data++] = USART3->DR; n9 x1 w { A. M( u) \3 Y/ b
- USART3_RX_BUF[USART3_Data++] = USART_ReceiveData(USART3);& V* H1 b+ \& Z. T) y9 ~0 o0 \& @) p# ?
- // Res= USART_ReceiveData(USART3);" M& T& w& Z: _6 w
- // USART_SendData(USART1,Res);while(!(USART1->SR&USART_FLAG_TXE)); + H& k* D% C( |% _& y
- }$ l5 B/ w; O: Y1 ^
* J5 d" N8 B% h- else if(USART_GetITStatus(USART3, USART_IT_IDLE) != RESET)
* j* u5 a9 C8 h - {
# [& V# W' G/ ? _& J - clear3=USART3->SR; //读SR寄存器 可以清空寄存器" T/ ~- F& ]! y+ G/ F# t5 b
- clear3=USART3->DR; //读DR寄存器(先读SR寄存器,再读DR,为了清除IDLE中断) 3 q5 I/ t7 T- j
- USART3_RX_STA=1; //标记接收到了一帧数据
" }) m0 N- a5 N! O6 [7 Q3 @, O - //USART3_Data=0;$ M' ^! N* u- u4 m( n, P
- } //enocean是不是读不到一帧数据,不用一帧数据测试一下
+ k! U1 b4 W1 D3 D - }
复制代码
' L: d m& x1 ^. x E+ E' g, [1.2.1 HAL库接收HAL库接收方式一在 HAL 库函数接收的时候,其实也可以使用标准库上一样的中断标志使能: - MX_LPUART1_UART_Init();0 \0 K" l' A- ^' r5 b' P
- __HAL_UART_ENABLE_IT(&hlpuart1,UART_IT_RXNE);
复制代码
7 R5 ~: g$ O) ]# y7 _( WIDLE 中断使能: - __HAL_UART_ENABLE_IT(&huart2,UART_IT_IDLE);
复制代码 ) {; v% i z& | l+ y, a! Z
这种方式的处理方式,可以和标准库差不多。 在 HAL 库中,外设的中断的入口函数都放在stm32l0xx_it.c 这个文件夹中( 以STM32L051 为例),在这个文件中可以找到和标准库一样的 中断入口函数,我们可以进行如下处理: - void USART2_IRQHandler(void). e! L! a# ]; x
- {: w5 T1 B3 b) g; ^$ O) ^! b
- /* USER CODE BEGIN USART2_IRQn 0 */
$ ^) Z' }7 C* U
9 ^ Q" F7 e0 L+ g2 n& A- /* USER CODE END USART2_IRQn 0 */
2 T) j# Y d% Y- Q# d& W6 K - HAL_UART_IRQHandler(&huart2);
. H/ a* P; H/ M1 y+ r2 J& t - /* USER CODE BEGIN USART2_IRQn 1 */
/ S7 G3 ^( R0 C' v5 R1 M - if(__HAL_UART_GET_FLAG(&huart2,UART_FLAG_RXNE) == SET){* Z4 k$ H% W/ G6 b
- // USART_Enocean_BUF[Enocean_Data++] = huart2.Instance->RDR
% c- G/ k N% D Y% Y" j - // RXNE 数据处理,直接读取数据 * Y' N O$ I" J2 f1 l/ [
- }
8 b5 O# J1 i: ?- D- X - if((__HAL_UART_GET_FLAG(&huart2,UART_FLAG_IDLE) != RESET))
, f9 }* k2 G7 R* K# A$ Y) g2 S - {
4 t0 l; Q8 D5 Z/ C - __HAL_UART_CLEAR_IT(&huart2,UART_CLEAR_IDLEF); //Çå³ýÖжÏ
0 z* _8 o* Y0 y8 s - // ReceiveState = 1; j9 L) w. H+ n* Q2 O9 o8 o: Z4 @0 [
- // IDLE 数据处理,一帧数据接收标志位置位3 p* p# w5 r* e; a9 P
- }& R `7 V+ I5 z+ T1 W. N
- }
复制代码
7 E3 I: n( w, XHAL库接收方式二但是在于 HAL 中,还有一种比较常用的开启中断方法,不是直接使能中断,而是通过调用 HAL 库函数 HAL_UART_Receive_IT :
$ [$ N, c4 S6 }8 c1 t
3 ^1 o0 M2 V0 u7 L
( J0 M! _: A; E# M& h) w
4 r9 Y0 V; s0 ^$ }4 n! I一般的使用方式步骤: 6 Q1 h+ B) J. i! L) l' ^7 v# `
! c6 X$ }" z# W- S7 i8 S- n5 ?; a, a: J- T7 F
0 }5 j4 m' W7 v/ C- O2 [/ ?0 x3 f; d% [% z
, w9 ~/ n% t* t9 r5 U9 y
对于本次需要说明的问题,就是使用了 HAL_UART_Receive_IT 函数导致的,下文我们会说明,这里列出了基本的使用步骤。
. k9 o5 f- O& O. e% Y. g' E6 W 二、 收发同时串口卡死?
+ n( b- e1 N+ y* o2.1 问题说明最近测试部反馈,产品有些时候的下行没反应,这里所说的下行,其实就是串口接收。 霹雳扒拉一大堆多余的省略 ... ... 只说几个重点: 出问题的最后现象就是串口发送正常,但是永远接收不到数据了,其他程序正常运行。 出问题只存在于串口又有接收,又有发送的产品上。 产品发送一般是周期性的,但是接收是随机的,无线信号串口接收,所以产品的出问题的情况也是随机的,但是数据量大起来肯定就会出现永远接收不到的问题。
5 D1 ~4 E; h3 `/ N2.2 尝试的处理方式因为所有的一些都是按照正常流程设计的,按理来说实在是不知道为什么会这样,所以网上查询测试了好久,现在我把尝试的处理方式以及步骤记录说明一下: 3 [3 K) G2 I- n0 _$ G2 g* F
2.2.1 清除错误标志位在使用 HAL 库的时候,有4个错误 flag,如下图:
7 J. M" h8 B# o7 o, s
* i1 Y9 K. i+ o( l! g' O$ p9 G Q! d5 [. j4 J# B
& J, O# O' j3 _4 ^* }期初还以为是某些异常错误导致的,经过网上的的一些查询,刚开始是添加了清除错误标志位: - __HAL_UART_CLEAR_FLAG(&hlpuart1, UART_FLAG_PE);//清标志
3 }8 L# i$ `. U5 e& V/ J - __HAL_UART_CLEAR_FLAG(&hlpuart1, UART_FLAG_FE);
% }: s# x) x. W4 h1 L1 T% [ - __HAL_UART_CLEAR_FLAG(&hlpuart1, UART_FLAG_NE);
5 _" g: [$ l. t: @, K: r0 L4 | - __HAL_UART_CLEAR_FLAG(&hlpuart1, UART_FLAG_ORE);
复制代码
$ ]! c7 U; I& d. E* X4 X在需要的地方加上错误标志位清除,我是在清除串口缓存中处理的:
3 W; N$ ?/ r; [1 T& Z# F
+ Z& G& `! N; ^. E2 R. x* K# j5 p- ]4 K. n) b
, U$ q4 _& {, K0 _
# q" q( s4 Y( }3 d' F$ n, S: a4 m2 u2.2.2 串口溢出错误其实串口溢出错误在上面的已经清除过标志位,因为这个问题着实搞得我头有点大,所以看到网上前人的处理方式和上面直接清除不一样,还是试了一把。 这里简单说明一下,我还特意去看了下自己的 CubeMX 设置,在设置的时候 有一个 Overrun 错误标志位,平时我们设置都不一 定往下拉着看 = =!:
+ o" _5 Q, R6 X- }
! z9 `1 @0 G9 ^9 v$ w$ T/ Z1 c1 e然后确定了开启了串口溢出错误检测以后,我根据网上的方式,加了一个HAL_UART_ErrorCallback函数,其实就类似于HAL_UART_RxCpltCallback 函数:
0 h/ v6 i# E- ]$ w, J5 _2 m. G) @
4 O4 _7 _# g5 ?$ l4 A: `" w5 \; n% P7 F7 V3 l" [2 k7 X
自己加了一个 出错处理函数,其实现在看来,当然也是没有用的。 " ^# b% j) L I: S
2.2.3 HAL库的半双工处理?折腾了好长一段时间,其实一开始就知道问题在于 同时收发会出问题的情况,那么继续上网找问题。 最终确定了一个问题就是: 我们都知道 STM32 串口是全双工的, STM32 HAL库在处理接收的时候会锁一下串口一会,导致变成某个短时间的“半双工”。 这个时候如果同时收发就会出现问题,最后解决的办法在这篇文章中看到了:STM32 F103串口同时收发出现死锁问题解决办法 问题在于我们使用的HAL_UART_Receive_IT函数中,有对串口加锁的操作: 9 C0 Z- A l& S0 @
1 z2 O- A/ ?+ O0 W9 ]
虽然在后面有解锁: , S! n5 U9 ]1 U! |$ u# h, X2 n4 r
5 ?6 y8 |0 d T! B( Y* q A) z: f/ t
/ T# o' M7 d- v7 x* ^$ H9 S& B$ i
8 D9 C% m1 G; |7 v) x但是根据后期的解决方式来说,确实就是这个HAL_UART_Receive_IT函数的问题,最后使用的方式为,在产生一次中断以后开启的时候手动解锁:
8 [/ m- }& ^' y
/ L9 B* `6 r1 |- |
5 N, a. |/ M6 F# e
- void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)& j; a3 n6 P& o L: p3 Z) B
- {6 ?6 O. g; d! `. D8 m) V8 F& d
- if(huart->Instance == LPUART1){
! ]4 X# `6 T, |3 l% n6 b- |/ b, z - Enocean_Data++;
X8 z0 D8 w0 L - if(Enocean_Data > 98)Enocean_Data = 0;
5 Z6 S9 I% ]) g; y* m' J - while(HAL_UART_Receive_IT(&hlpuart1, (uint8_t *)&USART_Enocean_BUF[Enocean_Data], 1) != HAL_OK){
. Y' J ~, s/ F& A2 C0 g - hlpuart1.RxState = HAL_UART_STATE_READY;
, [9 t. k0 G, u - __HAL_UNLOCK(&hlpuart1);
9 i. g+ @; L7 U) F, G9 w8 t" ? - }# _ Q& H7 y" A' m# b0 r$ S
- }7 j: ]# S/ Z) t3 K1 p
- else if(huart->Instance==USART1)4 l& ]7 h" s) G
- { & g9 q6 C# `5 \4 g
- }( K! G& q, ^% i
- }
复制代码 j3 q: M. e0 ]
' @- f1 n# z5 Z8 ~# \+ O z; K% f终于,串口不再卡死 , 成功!
+ C2 k: u6 e3 ]/ F( H' s; v3 G+ l" _" ]+ a3 x! ^: m3 K% h
转载自:矜辰所致* s' n$ Y* d+ ]9 N0 L
如有侵权请联系删除3 o. h" w% Y) ^$ j) A
7 N5 \& v* b5 _; d
P7 S# F" u7 \ |
直接使用HAL_UART_Transmit_IT,不会卡死,不过调用的时候要保证上一帧已经发送完成,不然会丢数据。
我觉得吧,一般情况使用HAL库收发函数要成对使用,接收和发送要么都轮询,要么都中断,要么都DMA,这样不用做特殊处理问题不大。
特殊情况下,比如RS485通信,收发不同时发生,轮询发+中断收也没问题。