串口

RZ/G2L串口简介

瑞萨RZ/G2L的串口简称SCI,全称Serial Communication Interface。

RZ/G2L有两种串口,一种带FIFO叫SCIFA,另一种不带FIFO叫SCIg。

1.png

所以,RZ/G2L有5路SCIFA和2路SCIg总共7路的串口。

RZ/G2L串口驱动

包含驱动代码和设备树两部分:

内核中的驱动文件sh-sci.c

该源码文件路径:drivers/tty/serial/sh-sci.c,该驱动代码包含SCIFA和SCIg两种型号的完整驱动代码。

编译内核的时候需进行以下配置

egrep -rin "CONFIG_SERIAL_SH_SCI" .out/.config
2180:CONFIG_SERIAL_SH_SCI=y
2181:CONFIG_SERIAL_SH_SCI_NR_UARTS=18
2182:CONFIG_SERIAL_SH_SCI_CONSOLE=y
2183:CONFIG_SERIAL_SH_SCI_EARLYCON=y
2184:CONFIG_SERIAL_SH_SCI_DMA=y

该配置能够确保sh-sci.c的驱动代码能够被编译包含到Image中。

github上提供的内核源码的defconfig中默认已包含SCI驱动代码的编译。

SCI串口的设备树节点在r9a07g044.dtsi中定义,分别定义了scif0~4, sci0~1共7路。

scif0: serial@1004b800 
{      
    compatible = "renesas,scif-r9a07g044";      
    reg = <0 0x1004b800 0 0x400>;      
    interrupts = <GIC_SPI 380 IRQ_TYPE_LEVEL_HIGH>,             
                 <GIC_SPI 382 IRQ_TYPE_LEVEL_HIGH>,             
                 <GIC_SPI 383 IRQ_TYPE_LEVEL_HIGH>,             
                 <GIC_SPI 381 IRQ_TYPE_LEVEL_HIGH>,             
                 <GIC_SPI 384 IRQ_TYPE_LEVEL_HIGH>,             
                 <GIC_SPI 384 IRQ_TYPE_LEVEL_HIGH>;      
    interrupt-names = "eri", "rxi", "txi",            
                 "bri", "dri", "tei";      
    clocks = <&cpg CPG_MOD R9A07G044_SCIF0_CLK_PCK>;      
    clock-names = "fck";      dmas = <&dmac 0x4e79>, <&dmac 0x4e7a>;      
    dma-names = "tx", "rx";      power-domains = <&cpg>;      
    resets = <&cpg R9A07G044_SCIF0_RST_SYSTEM_N>;      
    status = "disabled";    
};

scif1: serial@1004bc00 
{      
    compatible = "renesas,scif-r9a07g044";      
    reg = <0 0x1004bc00 0 0x400>;      
    interrupts = <GIC_SPI 385 IRQ_TYPE_LEVEL_HIGH>,             
                 <GIC_SPI 387 IRQ_TYPE_LEVEL_HIGH>,             
                 <GIC_SPI 388 IRQ_TYPE_LEVEL_HIGH>,             
                 <GIC_SPI 386 IRQ_TYPE_LEVEL_HIGH>,             
                 <GIC_SPI 389 IRQ_TYPE_LEVEL_HIGH>,             
                 <GIC_SPI 389 IRQ_TYPE_LEVEL_HIGH>;      
    interrupt-names = "eri", "rxi", "txi",            
                 "bri", "dri", "tei";      
    clocks = <&cpg CPG_MOD R9A07G044_SCIF1_CLK_PCK>;      
    clock-names = "fck";      
    dmas = <&dmac 0x4e7d>, <&dmac 0x4e7e>;      
    dma-names = "tx", "rx";      
    power-domains = <&cpg>;      
    resets = <&cpg R9A07G044_SCIF1_RST_SYSTEM_N>;      
    status = "disabled";    
};
    
scif2: serial@1004c000 
{      
    compatible = "renesas,scif-r9a07g044";      
    reg = <0 0x1004c000 0 0x400>;      
    interrupts = <GIC_SPI 390 IRQ_TYPE_LEVEL_HIGH>,             
                 <GIC_SPI 392 IRQ_TYPE_LEVEL_HIGH>,             
                 <GIC_SPI 393 IRQ_TYPE_LEVEL_HIGH>,             
                 <GIC_SPI 391 IRQ_TYPE_LEVEL_HIGH>,             
                 <GIC_SPI 394 IRQ_TYPE_LEVEL_HIGH>,             
                 <GIC_SPI 394 IRQ_TYPE_LEVEL_HIGH>;      
    interrupt-names = "eri", "rxi", "txi",            
                 "bri", "dri", "tei";     
     clocks = <&cpg CPG_MOD R9A07G044_SCIF2_CLK_PCK>;      
     clock-names = "fck";      
     dmas = <&dmac 0x4e81>, <&dmac 0x4e82>;      
     dma-names = "tx", "rx";      
     power-domains = <&cpg>;      
     resets = <&cpg R9A07G044_SCIF2_RST_SYSTEM_N>;      
     status = "disabled";    
 };
    
scif3: serial@1004c400 
{      
    compatible = "renesas,scif-r9a07g044";      
    reg = <0 0x1004c400 0 0x400>;      
    interrupts = <GIC_SPI 395 IRQ_TYPE_LEVEL_HIGH>,             
                 <GIC_SPI 397 IRQ_TYPE_LEVEL_HIGH>,             
                 <GIC_SPI 398 IRQ_TYPE_LEVEL_HIGH>,             
                 <GIC_SPI 396 IRQ_TYPE_LEVEL_HIGH>,             
                 <GIC_SPI 399 IRQ_TYPE_LEVEL_HIGH>,             
                 <GIC_SPI 399 IRQ_TYPE_LEVEL_HIGH>;      
    interrupt-names = "eri", "rxi", "txi",           
                  "bri", "dri", "tei";     
    clocks = <&cpg CPG_MOD R9A07G044_SCIF3_CLK_PCK>;      
    clock-names = "fck";      
    dmas = <&dmac 0x4e85>, <&dmac 0x4e86>;      
    dma-names = "tx", "rx";      
    power-domains = <&cpg>;      
    resets = <&cpg R9A07G044_SCIF3_RST_SYSTEM_N>;      
    status = "disabled";    
};
    
scif4: serial@1004c800 
{      
    compatible = "renesas,scif-r9a07g044";      
    reg = <0 0x1004c800 0 0x400>;      
    interrupts = <GIC_SPI 400 IRQ_TYPE_LEVEL_HIGH>,             
                 <GIC_SPI 402 IRQ_TYPE_LEVEL_HIGH>,             
                 <GIC_SPI 403 IRQ_TYPE_LEVEL_HIGH>,             
                 <GIC_SPI 401 IRQ_TYPE_LEVEL_HIGH>,             
                 <GIC_SPI 404 IRQ_TYPE_LEVEL_HIGH>,             
                 <GIC_SPI 404 IRQ_TYPE_LEVEL_HIGH>;      
    interrupt-names = "eri", "rxi", "txi",            
                 "bri", "dri", "tei";      
    clocks = <&cpg CPG_MOD R9A07G044_SCIF4_CLK_PCK>;      
    clock-names = "fck";      
    dmas = <&dmac 0x4e89>, <&dmac 0x4e8a>;      
    dma-names = "tx", "rx";      
    power-domains = <&cpg>;      
    resets = <&cpg R9A07G044_SCIF4_RST_SYSTEM_N>;      
    status = "disabled";    
};
    
sci0: serial@1004d000 
{      
    compatible = "renesas,r9a07g044-sci", "renesas,rz-sci";      
    reg = <0 0x1004d000 0 0x400>;      
    interrupts = <GIC_SPI 405 IRQ_TYPE_LEVEL_HIGH>,             
                 <GIC_SPI 406 IRQ_TYPE_EDGE_RISING>,             
                 <GIC_SPI 407 IRQ_TYPE_EDGE_RISING>,             
                 <GIC_SPI 408 IRQ_TYPE_LEVEL_HIGH>;      
    interrupt-names = "eri", "rxi", "txi", "tei";      
    clocks = <&cpg CPG_MOD R9A07G044_SCI0_CLKP>;      
    clock-names = "fck";      
    power-domains = <&cpg>;      
    resets = <&cpg R9A07G044_SCI0_RST>;      
    status = "disabled";    
};
    
sci1: serial@1004d400 
{      
    compatible = "renesas,r9a07g044-sci", "renesas,rz-sci";      
    reg = <0 0x1004d400 0 0x400>;      
    interrupts = <GIC_SPI 409 IRQ_TYPE_LEVEL_HIGH>,             
                 <GIC_SPI 410 IRQ_TYPE_EDGE_RISING>,            
                 <GIC_SPI 411 IRQ_TYPE_EDGE_RISING>,             
                 <GIC_SPI 412 IRQ_TYPE_LEVEL_HIGH>;      
    interrupt-names = "eri", "rxi", "txi", "tei";      
    clocks = <&cpg CPG_MOD R9A07G044_SCI1_CLKP>;      
    clock-names = "fck";      
    power-domains = <&cpg>;      
    resets = <&cpg R9A07G044_SCI1_RST>;      
    status = "disabled";    
};

设备树文件路径

arch/arm64/boot/dts/renesas/r9a07g044.dtsi

根据项目需要,使能需要使用的设备节点,如欲使用SCIFA2:

&scif2 
{  
    pinctrl-0 = <&scif2_pins>;  
    pinctrl-names = "default";  
    uart-has-rtscts;  
    status = "okay";
};

注意需通过scif2_pins正确处理IO口复用问题。如果不使用DMA需要将设备节点中的dmas和dma-names删除。

编译内核:

export ARCH=arm64
export CROSS_COMPILE=aarch64-none-elf-
export PATH=$PATH:/opt/arm/gcc-arm-10.2-2020.11-x86_64-aarch64-none-elf/bin
make defconfig O=.out && make -j8 O=.out

使用当前编译生成的内核Image和dtb

.out/arch/arm64/boot/Image 
.out/arch/arm64/boot/dts/renesas/r9a07g044l2-smarc.dtb

启动板子后就能在系统路径下生成/dev/ttySC2节点。

Linux应用层使用RZ/G2L的串口SCI

RZ/G2L的串口设备节点在Linux应用层遵循POSIX标准,使用方法和PC端的Ubuntu系统并无差别。

RZ/G2L除了支持市面上常用的串口波特率如9600/115200/921600等,实际上除POSIX系统定义的波特率都支持外,RZ/G2L能够支持的最大串口波特率是12.5Mbps,下一篇我们将介绍如何在内核驱动代码中实现RZ/G2L的最大波特率12.5Mbps。

如需了解更详细的使用方法请参考如下网站:

1、瑞萨官网

https://www.renesas.cn/cn/zh/products/microcontrollers-microprocessors/rz-mpus/rzg2l-getting-started 

2、RZ产品WIKI网站

https://renesas.info/wiki/Main_Page 

您可点击下方网址进入瑞萨中文论坛查看:

https://community-ja.renesas.com/zh/forums-groups/mcu-mpu/ 

RZ/G2L支持的最大波特率

2.png

RZ/G2L的SCIFA异步通讯模式下支持的最高波特率可以达到12.5Mbps,如果异步基础时钟选择16倍波特率,同时关闭波特率发生器的倍频模式下依然可以达到3.125Mbps。如果异步基础时钟选择8倍波特率或者波特率发生器开启倍频模式,最大波特率可以达到6.25Mbps。

在上集中我们有讲过RZ/G2L在Linux下的使用遵循POSIX标准。只要POSIX支持的波特率,RZ/G2L都可以支持,并且支持各种波特率下的误差修正,需要开启MDDRS寄存器。

Linux下串口的波特率

Linux下termbits.h支持的波特率如下

/* c_cflag bit meaning */
#define CBAUD   0000377
#define  B0 0000000     /* hang up */
#define  B50    0000001#define  B75    0000002
#define  B110   0000003#define  B134   0000004
#define  B150   0000005#define  B200   0000006
#define  B300   0000007#define  B600   0000010
#define  B1200  0000011#define  B1800  0000012
#define  B2400  0000013#define  B4800  0000014
#define  B9600  0000015#define  B19200 0000016
#define  B38400 0000017#define  EXTA   B19200
#define  EXTB   B38400#define  CBAUDEX 0000000
#define  B57600   00020#define  B115200  00021
#define  B230400  00022#define  B460800  00023
#define  B500000  00024#define  B576000  00025
#define  B921600  00026#define B1000000  00027
#define B1152000  00030#define B1500000  00031
#define B2000000  00032#define B2500000  00033
#define B3000000  00034#define B3500000  00035
#define B4000000  00036

也就是标准的Linux支持的最大波特率是4Mbps,但并不是4Mbps以下任意一个波特率都可以支持,只有30种选择。

那如果在特殊的应用场景中,需要这30种波特率以外的选择,是否能够实现呢?答案是肯定的,但是比较复杂。

这里我们提供一种Linux下实现非POSIX标准串口波特率的方法给大家参考。

Linux串口非标波特率的实现

涉及两部分他,包括内核和应用层

首先第一步:我们需要修改内核中的串口驱动,确保串口驱动能够支持需要添加的非标波特率。上集我们已经分享过RZ/G2L的串口驱动代码路径是drivers/tty/serial/sh-sci.c,目前通过开启MDDRS,RZ/G2L几乎可以支持12.5Mbps以下的任意串口波特率。

这里我们以前面提到的3.125Mbps/6.25Mbps/12.5Mbps为例,github上下载的sh-sci.c驱动默认并没有开启波特率发生器的倍频模式,异步基础时钟选择的是默认的16倍波特率。所以最大的波特率可以支持到3.125Mbps,如果需要支持6.25Mbps或者更高的12.5Mbps,需要开启波特率发生器的倍频模式,并且允许异步基础时钟选择8倍波特率。

+       #if ABCS0_BGDM_EN
+           if(baud > 6250000){
+               //SEMR_BGDM:Baud rate generator double-speed mode Select:
+               //SEMR_ABCS0:Asynchronous Base Clock Select:
+               serial_port_out(port, SEMR,
+                       serial_port_in(port, SEMR) | (SEMR_ABCS0 | SEMR_BGDM));
+               freq *= 2;
+               prediv /= 2;
+           }else if(baud > 3125000){
+               //SEMR_BGDM:Baud rate generator double-speed mode Select:
+               serial_port_out(port, SEMR,
+                   serial_port_in(port, SEMR) | SEMR_BGDM);
+               freq *= 2;
+           }
+       #endif

这部分代码与RZ/G2L的平台相关,需要根据RZ/G2L的规格书配置对应的寄存器。

第二步:为了允许应用层配置我们添加的这三种波特率,需要修改drivers/tty/tty_baudrate.c和include/uapi/asm-generic/termbits.h,这两个文件与平台无关。想要在内核中添加系统默认的30种波特率以外的波特率都需要修改这两个文件。这两个文件的修改内容可以参考以下:

diff --git a/drivers/tty/tty_baudrate.c b/drivers/tty/tty_baudrate.c
index bdfaee2c1331..75d287893d11 100644
--- a/drivers/tty/tty_baudrate.c
+++ b/drivers/tty/tty_baudrate.c
@@ -24,7 +24,7 @@ static const speed_t baud_table[] = {    
    1000000, 1152000, 1500000, 2000000 
#else    
    500000, 576000, 921600, 1000000, 1152000, 1500000, 2000000,
-   2500000, 3000000, 3500000, 4000000
+   2500000, 3000000, 3500000, 4000000, 3125000, 6250000, 12500000 
  #endif 
}; 

@@ -36,7 +36,7 @@ static const tcflag_t baud_bits[] = {    
    B1000000, B1152000, B1500000, B2000000 
#else    
    B500000, B576000, B921600, B1000000, B1152000, B1500000, B2000000,
-   B2500000, B3000000, B3500000, B4000000
+   B2500000, B3000000, B3500000, B4000000, B3125000, B6250000, B12500000 
#endif 
};
 
@@ -73,6 +73,14 @@ speed_t tty_termios_baud_rate(struct ktermios *termios)        
    else            
    cbaud += 15;    
    }
+   if (cbaud & CBAUDEX2) {
+       cbaud &= ~CBAUDEX2;
+
+       if (cbaud < 1 || cbaud + 30 > n_baud_table)
+           termios->c_cflag &= ~CBAUDEX2;
+       else
+           cbaud += 30;
+   }    
    return cbaud >= n_baud_table ? 0 : baud_table[cbaud]; 
} 
  EXPORT_SYMBOL(tty_termios_baud_rate);
diff --git a/include/uapi/asm-generic/termbits.h b/include/uapi/asm-generic/termbits.h
index 7db62a33ee52..1353300b6934 100644
--- a/include/uapi/asm-generic/termbits.h
+++ b/include/uapi/asm-generic/termbits.h
@@ -110,7 +110,7 @@ struct ktermios { 
  #define   FF1  0100000  
  
/* c_cflag bit meaning */
-#define CBAUD  0010017
+#define CBAUD  0030017 
 #define  B0    0000000     /* hang up */ 
 #define  B50   0000001 
 #define  B75   0000002

@@ -158,7 +158,9 @@ struct ktermios { 
 #define  B3500000 0010016 
 #define  B4000000 0010017
+#define CBAUDEX2 0020000
+#define  B3125000  0020001
+#define  B6250000  0020002
+#define  B12500000 0020003 
 #define CIBAUD   002003600000  /* input baud rate */ 
 #define CMSPAR   010000000000  /* mark or space (stick) parity */ 
 #define CRTSCTS      020000000000  /* flow control */

经过上面两步修改,内核已支持我们需要添加的3种非POSIX标准的串口波特率。

接下来演示应用层如何使用我们添加的这三种串口波特率。

#define B3125000  0020001
#define B6250000  0020002
#define B12500000 0020003
**
** 串口配置
** 参数 cfg 指向一个 uart_cfg_t 结构体对象
**/
static int uart_cfg(const uart_cfg_t *cfg)
{    
    struct termios new_cfg = {0}; //将 new_cfg 对象清零    
    speed_t speed;        
    
    /* 设置为原始模式 */    
    cfmakeraw(&new_cfg);        
    
    /* 使能接收 */    
    new_cfg.c_cflag |= CREAD| CLOCAL;        
    
    /* 设置波特率 */  
    speed = B3125000; // B3125000  B6250000  B12500000
    new_cfg.c_cflag |= speed;
    
    /* 串口的其他属性配置参考标准的POSIX */
    
    /* 写入配置、使配置生效 */    
    if (0 > tcsetattr(fd, TCSANOW, &new_cfg)) 
    {        
        fprintf(stderr, "tcsetattr error: %s\n", strerror(errno));        
        return -1;    
    }

经过以上修改,我们就可以在linux下使用文章开头提到的RZ/G2L的最大波特率12.5Mbps进行串口通讯。

需要注意的是,我们给RZ/G2L添加的这三个波特率尤其是6.25Mbps或者12.5Mbps远超标准linux下支持的最大波特率4Mbps,所以,通过PC端的Ubuntu是无法使用这三种波特率与RZ/G2L的SMARC EVK板进行通讯测试的,如果要使用我们上面添加的这三种波特率,只能在两个SMARC EVK板上进行。

所以,除以上添加的这三种波特率外,如果要添加POSIX标准支持的30种以外的其他波特率,都可以参考这个方法来实现。

如需了解更详细的使用方法请参考如下网站:

1、瑞萨官网

https://www.renesas.cn/cn/zh/products/microcontrollers-microprocessors/rz-mpus/rzg2l-getting-started 

2、RZ产品WIKI网站

https://renesas.info/wiki/Main_Page 

您可点击下方网址进入瑞萨中文论坛查看:

https://community-ja.renesas.com/zh/forums-groups/mcu-mpu/ 

来源:瑞萨嵌入式小百科:RZ/G2L串口SCI的使用(上)RZ/G2L串口SCI的使用(下)

免责声明:本文为转载文章,转载此文目的在于传递更多信息,版权归原作者所有。本文所用视频、图片、文字如涉及作品版权问题,请联系小编进行处理(联系邮箱:cathy@eetrend.com)。

围观 50

虽然现在通信方式有很多类型,但串口依然是嵌入式领域应用最广泛的通信方式之一。

本文给介绍如何使用带FIFO的串口来减少接收中断次数,通过一种自定义通讯协议格式,给出帧打包方法;之后介绍一种特殊的串口数据发送方法,可在避免使用串口发送中断的情况下,提高系统的响应速度。

1.简介

串口由于使用简单,价格低廉,配合RS485芯片可以实现长距离、抗干扰能力强的局域网络而被广泛使用。随着产品功能的增多,需要处理的任务也越来越复杂,系统任务也越来越需要及时响应。绝大多数的现代单片机(ARM7、Cortex-M3)串口都带有一定数量的硬件FIFO,本文将介绍如何使用硬件FIFO来减少接收中断次数,提高发送效率。在此之前,先来列举一下传统串口数据收发的不足之处:每接收一个字节数据,产生一次接收中断。不能有效的利用串口硬件FIFO,减少中断次数。应答数据采用等待发送的方法。由于串行数据传输的时间远远跟不上CPU的处理时间,等待串口发送完当前字节再发送下一字节会造成CPU资源浪费,不利于系统整体响应(在1200bps下,发送一字节大约需要10ms,如果一次发送几十个字节数据,CPU会长时间处于等待状态)。应答数据采用中断发送。增加一个中断源,增加系统的中断次数,这会影响系统整体稳定性(从可靠性角度考虑,中断事件应越少越好)。针对上述的不足之处,将结合一个常用自定义通讯协议,提供一个完整的解决方案。

2.串口FIFO

串口FIFO可以理解为串口专用的缓存,该缓存采用先进先出方式。数据接收FIFO和数据发送FIFO通常是独立的两个硬件。串口接收的数据,先放入接收FIFO中,当FIFO中的数据达到触发值(通常触发值为1、2、4、8、14字节)或者FIFO中的数据虽然没有达到设定值但是一段时间(通常为3.5个字符传输时间)没有再接收到数据,则通知CPU产生接收中断;发送的数据要先写入发送FIFO,只要发送FIFO未空,硬件会自动发送FIFO中的数据。写入发送FIFO的字节个数受FIFO最大深度影响,通常一次写入最多允许16字节。上述列举的数据跟具体的硬件有关,CPU类型不同,特性也不尽相同,使用前应参考相应的数据手册。

3.数据接收与打包

FIFO可以缓存串口接收到的数据,因此我们可以利用FIFO来减少中断次数。以NXP的lpc1778芯片为例,接收FIFO的触发级别可以设置为1、2、4、8、14字节,推荐使用8字节或者14字节,这也是PC串口接收FIFO的默认值。这样,当接收到大量数据时,每8个字节或者14个字节才会产生一次中断(最后一次接收除外),相比接收一个字节即产生一个中断,这种方法串口接收中断次数大大减少。

将接收FIFO设置为8或者14字节也十分简单,还是以lpc1778为例,只需要设置UART FIFO控制寄存器UnFCR即可。

接收的数据要符合通讯协议规定,数据与协议是密不可分的。通常我们需要将接收到的数据根据协议打包成一帧,然后交由上层处理。下面介绍一个自定义的协议帧格式,并给出一个通用打包成帧的方法。

自定义协议格式如图3-1所示。

1.png

  • 帧首:通常是3~5个0xFF或者0xEE

  • 地址号:要进行通讯的设备的地址编号,1字节

  • 命令号:对应不同的功能,1字节

  • 长度:数据区域的字节个数,1字节

  • 数据:与具体的命令号有关,数据区长度可以为0,整个帧的长度不应超过256字节

  • 校验:异或和校验(1字节)或者CRC16校验(2字节),本例使用CRC16校验

下面介绍如何将接收到的数据按照图3-1所示的格式打包成一帧。

3.1 定义数据结构

typedef struct {  
      uint8_t * dst_buf;                  //指向接收缓存  
      uint8_t sfd;                        //帧首标志,为0xFF或者0xEE  
      uint8_t sfd_flag;                   //找到帧首,一般是3~5个FF或EE  
      uint8_t sfd_count;                  //帧首的个数,一般3~5个  
      uint8_t received_len;               //已经接收的字节数  
      uint8_t find_fram_flag;             //找到完整帧后,置1  
      uint8_t frame_len;                  //本帧数据总长度,这个区域是可选的  
}find_frame_struct;

3.2 初始化数据结构,一般放在串口初始化中

/** 
* @brief    初始化寻找帧的数据结构 
* @param    p_fine_frame:指向打包帧数据结构体变量 
* @param    dst_buf:指向帧缓冲区 
* @param    sfd:帧首标志,一般为0xFF或者0xEE 
*/  
void init_find_frame_struct(find_frame_struct * p_find_frame,uint8_t *dst_buf,uint8_t sfd)  
{  
     p_find_frame->dst_buf=dst_buf;  
     p_find_frame->sfd=sfd;  
     p_find_frame->find_fram_flag=0;  
     p_find_frame->frame_len=10;       
     p_find_frame->received_len=0;  
     p_find_frame->sfd_count=0;  
     p_find_frame->sfd_flag=0;  
}

3.3 数据打包程序

/** 
* @brief    寻找一帧数据  返回处理的数据个数 
* @param    p_find_frame:指向打包帧数据结构体变量 
* @param    src_buf:指向串口接收的原始数据 
* @param    data_len:src_buf本次串口接收到的原始数据个数 
* @param    sum_len:帧缓存的最大长度 
* @return   本次处理的数据个数 
*/  
uint32_t find_one_frame(find_frame_struct * p_find_frame,const uint8_t * src_buf,uint32_t data_len,uint32_t sum_len)  
{  
     uint32_t src_len=0;  
       
     while(data_len--)  
     {  
         if(p_find_frame ->sfd_flag==0)                        
         {   //没有找到起始帧首  
             if(src_buf[src_len++]==p_find_frame ->sfd)  
             {  
                 p_find_frame ->dst_buf[p_find_frame ->received_len++]=p_find_frame ->sfd;  
                 if(++p_find_frame ->sfd_count==5)          
                 {  
                     p_find_frame ->sfd_flag=1;  
                     p_find_frame ->sfd_count=0;  
                     p_find_frame ->frame_len=10;  
                 }  
             }  
             else  
             {  
                 p_find_frame ->sfd_count=0;   
                 p_find_frame ->received_len=0;   
             }  
         }  
         else   
         {   //是否是"长度"字节? Y->获取这帧的数据长度  
             if(7==p_find_frame ->received_len)                
             {  
            p_find_frame->frame_len=src_buf[src_len]+5+1+1+1+2; //帧首+地址号+命令号+数据长度+校验  
                   
                 if(p_find_frame->frame_len>=sum_len)  
                 {   //这里处理方法根据具体应用不一定相同  
                     MY_DEBUGF(SLAVE_DEBUG,("数据长度超出缓存!\n"));  
                     p_find_frame->frame_len= sum_len;       
                 }  
             }  
               
             p_find_frame ->dst_buf[p_find_frame->received_len++]=src_buf[src_len++];  
               
             if(p_find_frame ->received_len==p_find_frame ->frame_len)                  
             {  
                 p_find_frame ->received_len=0;              //一帧完成    
                 p_find_frame ->sfd_flag=0;  
                 p_find_frame ->find_fram_flag=1;   
                      
                 return src_len;  
             }  
         }  
     }  
     p_find_frame ->find_fram_flag=0;  
     return src_len;  
}

使用例子:

定义数据结构体变量:

find_frame_structslave_find_frame_srt;

定义接收数据缓冲区:

#define SLAVE_REC_DATA_LEN  128
uint8_t slave_rec_buf[SLAVE_REC_DATA_LEN];

在串口初始化中调用结构体变量初始化函数:

init_find_frame_struct(&slave_find_frame_srt,slave_rec_buf,0xEE);

在串口接收中断中调用数据打包函数:

find_one_frame(&slave_find_frame_srt,tmp_rec_buf,data_len,SLAVE_REC_DATA_LEN);

其中,rec_buf是串口接收临时缓冲区,data_len是本次接收的数据长度。

4.数据发送

前文提到,传统的等待发送方式会浪费CPU资源,而中断发送方式虽然不会造成CPU资源浪费,但又增加了一个中断源。在我们的使用中发现,定时器中断是几乎每个应用都会使用的,我们可以利用定时器中断以及硬件FIFO来进行数据发送,通过合理设计后,这样的发送方法即不会造成CPU资源浪费,也不会多增加中断源和中断事件。

需要提前说明的是,这个方法并不是对所有应用都合适,对于那些没有开定时器中断的应用本方法当然是不支持的,另外如果定时器中断间隔较长而通讯波特率又特别高的话,本方法也不太适用。公司目前使用的通讯波特率一般比较小(1200bps、2400bps),在这些波特率下,定时器间隔为10ms以下(含10ms)就能满足。如果定时器间隔为1ms以下(含1ms),是可以使用115200bps的。

本方法主要思想是:定时器中断触发后,判断是否有数据要发送,如果有数据要发送并且满足发送条件,则将数据放入发送FIFO中,对于lpc1778来说,一次最多可以放16字节数据。之后硬件会自动启动发送,无需CPU参与。

下面介绍如何使用定时器发送数据,硬件载体为RS485。因为发送需要操作串口寄存器以及RS485方向控制引脚,需跟硬件密切相关,以下代码使用的硬件为lpc1778,但思想是通用的。

4.1 定义数据结构

/*串口帧发送结构体*/  
typedef struct {  
      uint16_t send_sum_len;          //要发送的帧数据长度  
      uint8_t  send_cur_len;          //当前已经发送的数据长度  
      uint8_t  send_flag;             //是否发送标志  
      uint8_t * send_data;            //指向要发送的数据缓冲区  
}uart_send_struct;

4.2 定时处理函数

/** 
* @brief    定时发送函数,在定时器中断中调用,不使用发送中断的情况下减少发送等待 
* @param    UARTx:指向硬件串口寄存器基地址 
* @param    p:指向串口帧发送结构体变量 
*/  
#define FARME_SEND_FALG 0x5A          
#define SEND_DATA_NUM   12  
static void uart_send_com(LPC_UART_TypeDef *UARTx,uart_send_struct *p)  
{  
     uint32_t i;  
     uint32_t tmp32;  
       
     if(UARTx->LSR &(0x01<<6))                      //发送为空  
     {         
         if(p->send_flag==FARME_SEND_FALG)  
         {                          
             RS485ClrDE;                             // 置485为发送状态  
               
             tmp32=p->send_sum_len-p->send_cur_len;  
             if(tmp32>SEND_DATA_NUM)                 //向发送FIFO填充字节数据  
             {  
                 for(i=0;i<SEND_DATA_NUM;i++)  
                 {  
                     UARTx->THR=p->send_data[p->send_cur_len++];  
                 }  
             }  
             else  
             {  
                 for(i=0;i<tmp32;i++)  
                 {  
                     UARTx->THR=p->send_data[p->send_cur_len++];  
                 }  
                 p->send_flag=0;                      
             }  
         }  
         else  
         {  
             RS485SetDE;  
         }  
     }  
}

其中,RS485ClrDE为宏定义,设置RS485为发送模式;RS485SetDE也为宏定义,设置RS485为接收模式。

使用例子:

定义数据结构体变量:

uart_send_struct uart0_send_str;

定义发送缓冲区:

uint8_t uart0_send_buf[UART0_SEND_LEN];

根据使用的硬件串口,对定时处理函数做二次封装:

void uart0_send_data(void)
{
   uart_send_com(LPC_UART0,&uart0_send_str);
}

将封装函数uart0_send_data();放入定时器中断处理函数中;

在需要发送数据的地方,设置串口帧发送结构体变量:

uart0_send_str.send_sum_len=data_len;       //data_len为要发送的数据长度
uart0_send_str.send_cur_len=0;              //固定为0
uart0_send_str.send_data=uart0_send_buf;    //绑定发送缓冲区
uart0_send_str.send_flag=FARME_SEND_FALG;   //设置发送标志

5.总结

本文主要讨论了一种高效的串口数据收发方法,并给出了具体的代码实现。在当前处理器任务不断增加的情况下,提供了一个占用资源少,可提高系统整体性能的新的思路。

来源:嵌入式专栏

免责声明:本文为转载文章,转载此文目的在于传递更多信息,版权归原作者所有。本文所用视频、图片、文字如涉及作品版权问题,请联系小编进行处理(联系邮箱:cathy@eetrend.com)。

围观 22

在上一讲,我们讲过CKS32F4xx系列的6个串口都支持DMA传输。因此本节我们对CKS32F4xx系列的DMA进行介绍,同时利用DMA对串口数据进行传输。

DMA介绍

DMA,全称为:Direct Memory Access,即直接存储器访问。DMA传输方式无需CPU直接控制传输,也没有中断处理方式那样保留现场和恢复现场的过程,通过硬件为RAM与I/O设备开辟一条直接传送数据的通路,能使CPU的效率大为提高。

CKS32F4xx系列最多有2个DMA控制器(DMA1和DMA2),共16个数据流(每个控制器8个),每个数据流总共可以有多达8个通道(或称请求)。CKS32F4xx系列的DMA支持外设到存储器传输、存储器到外设传输和存储器到存储器传输三种传输模式。,存储器一般是指片内SRAM、外部存储器、片内Flash等等。

外设到存储器:把外设数据寄存器内容转移到指定的内存空间。比如进行ADC采集时我们可以利用DMA传输把AD转换的数据转移到我们定义的存储区中。 

存储器到外设:把特定存储区内容转移至外设的数据寄存器中,这种多用于外设的发送通信,比如SPI、I2C和串口等。

存储器到存储器:就是把一个指定的存储区内容拷贝到另一个存储区空间。功能类似于C语言内存拷贝函数memcpy,不过利用DMA传输可以达到更高的传输效率。

注意:只有DMA2控制器支持存储器到存储器的传输,DMA1不支持。

DMA功能框图

1.png

标号1处是外设通道,每一个数据流(标号2)对应着8个外设通道,外设通道选择要解决的主要问题是决定哪一个外设作为该数据流的源地址或者目标地址。DMA1各个通道的请求映像如下表所示:

2.png

DMA2各个通道的请求映像如下表所示:

3.png

比如我们利用DMA将存储器数据传输到串口3,然后发送出去,那么根据上表可知,我们可以选择DMA1的数据流3通道4,或者DMA1的数据流4通道7。而将串口3的数据传输到存储器则可以选择DMA1的数据流1通道4。

标号2处是数据流和仲裁器,一个DMA控制器对应8个数据流,数据流包含要传输数据的源地址、目标地址、数据等等信息。如果我们需要同时使用同一个DMA控制器 (DMA1 或DMA2) 多个外设请求时,那必然需要同时使用多个数据流,这时就需要仲裁器对每一个数据流的优先级进行划分。数据流的优先级可以通过配置DMA_SxCR寄存器 PL[1:0]位,可以设置为非常高、高、中和 低四个级别。如果两个或以上数据流软件设置优先级一样,则他们优先级取决于数据流编号,编号越低越具有优先权,比如数据流2优先级高于数据流 3。

标号3处是FIFO,用于在源数据传输到目标地址之前临时存放数据用的。可以通过DMA数据流xFIFO控制寄存器DMA_SxFCR的FTH[1:0]位来控制FIFO的阈值,分别为1/4、1/2、3/4和满。如果数据存储量达到阈值级别时,FIFO内容将传输到目标中。

标号4和5处是存储器端口、外设端口。DMA控制器通过存储器端口和外设端口与存储器和外设进行数据传输。DMA2(DMA控制器2)的存储器端口和外设端口都是连接到AHB总线矩阵,可以使用AHB总线矩阵功能。而DMA1的存储区端口相比DMA2的要减少AHB2外设的访问权,同时DMA1外设端口是没有连接至总线矩阵的,只有连接到APB1外设,所以 DMA1不能实现存储器到存储器传输。

标号6处是编程端口,AHB从器件编程端口是连接至AHB2外设的,AHB2外设在使用 DMA传输时需要相关控制信号。

DMA软件配置

在标准库函数中对DMA进行初始化主要是对结构体DMA_InitTypeDef内的各类参数进行初始化。DMA_InitTypeDef结构体参数及介绍如下:

typedef struct
{  
    uint32_t DMA_Channel;            
    uint32_t DMA_PeripheralBaseAddr;   
    uint32_t DMA_Memory0BaseAddr;     
    uint32_t DMA_DIR;                 
    uint32_t DMA_BufferSize;          
    uint32_t DMA_PeripheralInc;       
    uint32_t DMA_MemoryInc;         
    uint32_t DMA_PeripheralDataSize;   
    uint32_t DMA_MemoryDataSize;      
    uint32_t DMA_Mode;               
    uint32_t DMA_Priority;            
    uint32_t DMA_FIFOMode;           
    uint32_t DMA_FIFOThreshold;       
    uint32_t DMA_MemoryBurst;       
    uint32_t DMA_PeripheralBurst;   
}DMA_InitTypeDef;

1)DMA_Channel:DMA请求通道的选择,每个外设对应固定的通道,具体设置值需要查25.2节中的DMA1和DMA2各个通道的请求映像表。该值在标准库中可供选择的参数值如下:

#define DMA_Channel_0                     ((uint32_t)0x00000000)
#define DMA_Channel_1                     ((uint32_t)0x02000000)
#define DMA_Channel_2                     ((uint32_t)0x04000000)
#define DMA_Channel_3                     ((uint32_t)0x06000000)
#define DMA_Channel_4                     ((uint32_t)0x08000000)
#define DMA_Channel_5                     ((uint32_t)0x0A000000)
#define DMA_Channel_6                     ((uint32_t)0x0C000000)
#define DMA_Channel_7                     ((uint32_t)0x0E000000)

2)DMA_PeripheralBaseAddr:外设地址,设定DMA_SxPAR寄存器的值;一般设置为外设的数据寄存器地址,如果是存储器到存储器模式则设置为其中一个存储区地址。比如要进行串口DMA传输,那么外设基地址为串口接收发送数据存储器USART1->DR的地址,表示方法为&USART1->DR。

3)DMA_Memory0BaseAddr:存储器0地址,设定DMA_SxM0AR寄存器值;一般设置为我们自定义存储区的首地址。比如我们程序中自己定义的用来存放数据的数组,此时这个值设置为数组名即可。

4) DMA_DIR:传输方向选择,可选外设到存储器、存储器到外设以及存储器到存储器。该值在标准库中可供选择的参数值如下:

DMA_DIR_PeripheralToMemory  //外设到存储器
DMA_DIR_MemoryToPeripheral  //存储器到外设
DMA_DIR_MemoryToMemory  //存储器到存储器

5)DMA_BufferSize:设定待传输数据数目,初始化设定DMA_SxNDTR寄存器的值。

6)DMA_PeripheralInc:是否使能外设地址自动递增功能,它设定DMA_SxCR寄存器的PINC位的值;一般外设都是只有一个数据寄存器,所以一般不会使能该位。该值在标准库中可供选择的参数值如下:

DMA_PeripheralInc_Enable      //使能
DMA_PeripheralInc_Disable     //不使能

7)DMA_MemoryInc:是否使能存储器地址自动递增功能,它设定DMA_SxCR寄存器的MINC位的值;我们自定义的存储区一般都是存放多个数据的,所以一般是使能存储器地址自动递增功能。该值在标准库中可供选择的参数值如下:

DMA_MemoryInc_Enable      //使能
DMA_MemoryInc_Disable     //不使能

8)DMA_PeripheralDataSize:外设数据宽度,可选字节(8位)、半字(16位)和字(32位),根据外设数据长度进行选择。该值在标准库中可供选择的参数值如下:

DMA_PeripheralDataSize_Byte      //字节
DMA_PeripheralDataSize_HalfWord  //半字
DMA_PeripheralDataSize_Word     //字

9) DMA_MemoryDataSize:存储器数据宽度,可选字节(8位)、半字(16位)和字(32位),和外设数据宽度相对应。该值在标准库中可供选择的参数值如下:

DMA_MemoryDataSize_Byte      //字节
DMA_MemoryDataSize_HalfWord  //半字
DMA_MemoryDataSize_Word     //字

10) DMA_Mode:DMA传输模式选择,可选一次传输或者循环传输。该值在标准库中可供选择的参数值如下:

DMA_Mode_Normal      //一次传输
DMA_Mode_Circular     //循环传输

11) DMA_Priority:软件设置数据流的优先级,有4个可选优先级分别为非常高、高、中和低。DMA优先级只有在多个DMA数据流同时使用时才有意义,如果只有一个数据流的话,设置成非常高优先级就可以了。该值在标准库中可供选择的参数值如下:

DMA_Priority_Low         //低
DMA_Priority_Medium     //中
DMA_Priority_High       //高
DMA_Priority_VeryHigh   //非常高

12) DMA_FIFOMode:FIFO模式使能,如果设置为DMA_FIFOMode_Enable 表示使能FIFO模式功能;如果采用直接传输模式,则不需要使用FIFO模式。直接模式下,DMA直接进行数据从源地址到目的地址的传输。而FIFO模式下,可以将要传输的多个数据(或字节)累计存储在FIFO缓冲器中,然后在FIFO缓冲器中设置存储阈值,当到达阈值时,FIFO会自动把所有存储的数据一次性的发送到目标地址。该值在标准库中可供选择的参数值如下:

DMA_FIFOMode_Disable  //不使能
DMA_FIFOMode_Enable  //使能

13) DMA_FIFOThreshold:FIFO阈值选择,可选4种状态分别为FIFO容量的1/4、1/2、3/4和满;不使用FIFO模式时,该设置改值无效。该值在标准库中可供选择的参数值如下:

DMA_FIFOThreshold_1QuarterFull         //1/4
DMA_FIFOThreshold_HalfFull            //1/2
DMA_FIFOThreshold_3QuartersFull       //3/4
DMA_FIFOThreshold_Full              //满

14) DMA_MemoryBurst:存储器突发模式选择,可选单次模式、4节拍的增量突发模式、8节拍的增量突发模式或16节拍的增量突发模式。单次传输模式下,一次操作(软件)只能传输一次,突发传输模式下,一次操作可以传输多次,如4次,8次,16次。该值在标准库中可供选择的参数值如下:

DMA_MemoryBurst_Single            //单次
DMA_MemoryBurst_INC4            //4节拍
DMA_MemoryBurst_INC8           //8节拍
DMA_MemoryBurst_INC16          //16节拍

15) DMA_PeripheralBurst:外设突发模式选择,可选单次模式、4节拍的增量突发模式、8节拍的增量突发模式或16节拍的增量突发模式。该值在标准库中可供选择的参数值如下:

DMA_PeripheralBurst_Single        //单次
DMA_PeripheralBurst_INC4         //4节拍
DMA_PeripheralBurst_INC8         //8节拍
DMA_PeripheralBurst_INC16        //16节拍

串口DMA接发通信实验

串口的DMA接发通信实验是存储器到外设和外设到存储器的数据传输。在第24课串口通信的基础上编写而成。

1.编程要点

1)配置USART通信功能;

2)配置DMA通信功能,存储器到外设和外设到存储器两种模式的配置。

3)使能指定的DMA数据流中断;

4)使能USART3的DMA发送和接收请求;

5)开始一次DMA传输。

2.代码分析

代码中有关串口的配置的程序在第24课已经详细讲过了,这里就不再讲述。主要是对DMA相关的代码进行分析,相关程序在dma.c文件里。

代码清单1:DMA初始化配置

void MYDMA_Config(void)
{   
    DMA_InitTypeDef  DMA_InitStructure;  
    NVIC_InitTypeDef NVIC_InitStructure;  
    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA1,ENABLE);  
    while (DMA_GetCmdStatus(DMA1_Stream3) != DISABLE){}   
    //存储器到外设的DMA配置  
    DMA_DeInit(DMA1_Stream3);  
    DMA_InitStructure.DMA_Channel = DMA_Channel_4;  //通道选择  
    DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)(&USART3->DR);//DMA外设地址  
    DMA_InitStructure.DMA_Memory0BaseAddr =(uint32_t)USART3_TX_BUF;//DMA 存储器0地址  
    DMA_InitStructure.DMA_DIR = DMA_DIR_MemoryToPeripheral;//存储器到外设模式  
    DMA_InitStructure.DMA_BufferSize = USART3_MAX_TX_LEN;//数据传输量   
    DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;//外设非增量模式  
    DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;//存储器增量模式  
    DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;//外设数据长度:8位  
    DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;//存储器数据长度:8位  
    DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;// 使用普通模式   
    DMA_InitStructure.DMA_Priority = DMA_Priority_High;//高优先级  
    DMA_InitStructure.DMA_FIFOMode = DMA_FIFOMode_Disable;           
    DMA_InitStructure.DMA_FIFOThreshold = DMA_FIFOThreshold_HalfFull;  
    DMA_InitStructure.DMA_MemoryBurst = DMA_MemoryBurst_Single;//存储器突发单次传输  
    DMA_InitStructure.DMA_PeripheralBurst = DMA_PeripheralBurst_Single;//外设突发单次传输  
    DMA_Init(DMA1_Stream3, &DMA_InitStructure);//初始化DMA Stream  
    DMA_ClearITPendingBit(DMA1_Stream3,DMA_IT_TCIF3);                         //清除标志位      
    DMA_Cmd(DMA1_Stream3, DISABLE);                                          //关闭DMA  
    DMA_ITConfig(DMA1_Stream3,DMA_IT_TC,ENABLE);     
    NVIC_InitStructure.NVIC_IRQChannel                   = DMA1_Stream3_IRQn;  
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;  
    NVIC_InitStructure.NVIC_IRQChannelSubPriority        = 0;  
    NVIC_InitStructure.NVIC_IRQChannelCmd                = ENABLE;  
    NVIC_Init(&NVIC_InitStructure);  USART_DMACmd(USART3,USART_DMAReq_Tx,ENABLE);       
    //外设到存储器的DMA配置  
    DMA_DeInit(DMA1_Stream1);  
    while (DMA_GetCmdStatus(DMA1_Stream1) != DISABLE){}//等待DMA可配置   
    DMA_InitStructure.DMA_Channel = DMA_Channel_4;  //通道选择  
    DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)(&USART3->DR);;//DMA外设地址  
    DMA_InitStructure.DMA_Memory0BaseAddr = (uint32_t)USART3_RX_BUF;//DMA 存储器0地址  
    DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralToMemory;//存储器到外设模式  
    DMA_InitStructure.DMA_BufferSize = USART3_MAX_RX_LEN;//数据传输量   
    DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;//外设非增量模式  
    DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;//存储器增量模式  
    DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;//外设数据长度:8位  
    DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;//存储器数据长度:8位  
    DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;// 使用普通模式   
    DMA_InitStructure.DMA_Priority = DMA_Priority_VeryHigh;//最高优先级  
    DMA_InitStructure.DMA_FIFOMode = DMA_FIFOMode_Disable;           
    DMA_InitStructure.DMA_FIFOThreshold = DMA_FIFOThreshold_HalfFull;  
    DMA_InitStructure.DMA_MemoryBurst = DMA_MemoryBurst_Single;//存储器突发单次传输  
    DMA_InitStructure.DMA_PeripheralBurst = DMA_PeripheralBurst_Single;//外设突发单次传输  
    DMA_Init(DMA1_Stream1, &DMA_InitStructure);//初始化DMA Stream  
    DMA_ClearITPendingBit(DMA1_Stream1,DMA_IT_TCIF1);                         //清除标志位      
    DMA_Cmd(DMA1_Stream1, ENABLE);                                          //关闭DMA  
    DMA_ITConfig(DMA1_Stream1,DMA_IT_TC,ENABLE);   //使能DMA1数据流1的传输完成中断  
    NVIC_InitStructure.NVIC_IRQChannel                   = DMA1_Stream1_IRQn;  
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;  
    NVIC_InitStructure.NVIC_IRQChannelSubPriority        = 0;  
    NVIC_InitStructure.NVIC_IRQChannelCmd                = ENABLE;  
    NVIC_Init(&NVIC_InitStructure);  
    USART_DMACmd(USART3,USART_DMAReq_Rx,ENABLE);     
}

这段代码主要是根据第25.3节中讲解的内容对DMA进行配置的。串口发送数据我们利用的是DMA数据流3通道4,存储器是我们定义的一个数组USART3_TX_BUF,外设是串口的数据寄存器USART_DR,表示方法为&USART3->DR。串口接收数据我们利用的是DMA数据流1通道4,存储器是我们定义的一个数组USART3_RX_BUF,外设是串口的数据寄存器USART_DR,表示方法为&USART3->DR。

代码清单2:DMA1_Stream3_IRQHandler函数

void DMA1_Stream3_IRQHandler(void)  //DMA发送数据流中断服务函数
{    
    if(DMA_GetITStatus(DMA1_Stream3,DMA_IT_TCIF3) != RESET)       
    {          
        DMA_ClearITPendingBit(DMA1_Stream3,DMA_IT_TCIF3);         
        DMA_Cmd(DMA1_Stream3,DISABLE); //关闭DMA          
        USART3_DMA_Tx_flag = 0; //发送完成    
    }       
}

该函数是DMA1数据流3中断服务函数,在函数里我们通过对DMA_IT_TCIF3标志位的判断,可以知道数据流3的数据是否传输完成,然后清除相应的标志位和关闭DMA传输。

代码清单3:DMA1_Stream1_IRQHandler函数

void DMA1_Stream1_IRQHandler(void) //DMA接收数据流中断服务函数
{    
    if(DMA_GetITStatus(DMA1_Stream1,DMA_IT_TCIF1) != RESET)       
    {          
        DMA_ClearITPendingBit(DMA1_Stream1,DMA_IT_TCIF1); //清除标志位            
        DMA_Cmd(DMA1_Stream1,DISABLE); //关闭DMA      
    }   
}

该函数的功能和DMA1数据流3中断服务函数的功能是一样的。

代码清单4:USART3_IRQHandler函数

void USART3_IRQHandler(void)                  
{   
    if(USART_GetITStatus(USART3 , USART_IT_IDLE) != RESET)     
    {    
        USART_ReceiveData(USART3);//清除空闲中断标志    
        DMA_Cmd(DMA1_Stream1,DISABLE);//关闭DMA ,防止干扰      
        USART3_RX_STA = USART3_MAX_RX_LEN - DMA_GetCurrDataCounter(DMA1_Stream1);//获得接收到的字节数      
        DMA_SetCurrDataCounter(DMA1_Stream1,USART3_MAX_RX_LEN);     
        DMA_ClearITPendingBit(DMA1_Stream1,DMA_IT_TCIF1);//比f103多的一句         
        DMA_Cmd(DMA1_Stream1,ENABLE);// DMA 开启,等待数据。    
        USART3_RX_BUF[USART3_RX_STA&0X7FFF]='\0';    
        USART3_RX_STA|=0x8000;   //最高位置1,标记接收完成了                                                                                                   
    }  
    if(USART_GetFlagStatus(USART3,USART_FLAG_ORE) == SET)  // 检查 ORE 标志,防止开关总中断死机,放在接收中断前面  
    {    
        USART_ClearFlag(USART3,USART_FLAG_ORE);    
        USART_ReceiveData(USART3);  
    }
}

该函数是串口3的中断服务函数。在函数里判断接收完成是通过串口空闲中断的方式实现,即当串口数据流停止后,就会产生IDLE中断。然后在中断里依次做了以下事情:处理接收buff的数据;关闭串口接收DMA通道,防止后面接收到数据产生串扰;计算接收到的字节数存储在USART3_RX_STA变量中;重新设置DMA下次要接收的数据字节数;清除DMA1数据流1接收完成标志位;开启DMA通道,等待下一次的数据接收;置位标志位USART3_RX_STA,表示接收完成了。同时为了防止串口溢出错误,程序卡死在串口中断里,在中断服务函数里增加了溢出错误的处理。

代码清单5:主函数

int main(void)
{  
    GPIO_Configuration();  
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//设置系统中断优先级分组2  
    USART_Configuration();  
    printf("start\r\n");  
    while (1)  
    {    
        if(USART3_RX_STA&0x8000)    
        {        
            //printf("USART3_Read:%s",USART3_RX_BUF);      
            printf("USART3_Read:%s",USART3_RX_BUF);      
            USART3_RX_STA=0;      
            memset(USART3_RX_BUF,0,sizeof(USART3_RX_BUF));    
        }  
    }
}

主函数的编写逻辑比较简单,首先是各类外设的初始化,包括GPIO初始化、NVIC中断初始化、串口初始化。然后在while循环里等待接收完成标志置位,将接收到的数据再发送到串口调试助手,并利用memset函数将存储数据的数组USART3_RX_BUF清零。

来源:中科芯MCU

免责声明:本文为转载文章,转载此文目的在于传递更多信息,版权归原作者所有。本文所用视频、图片、文字如涉及作品版权问题,请联系小编进行处理(联系邮箱:cathy@eetrend.com)。

围观 11

嵌入式开发中,UART串口是最常见的一种通信接口,你知道为啥串口这么常见吗?本文就带你深入了解串口最底层的本质内容。

一、什么是串口通讯?

串行通讯是指仅用一根接收线和一根发送线就能将数据以位进行传输的一种通讯方式。尽管串行通讯的比按字节传输的并行通信慢,但是串口可以在仅仅使用两根线的情况下就能实现数据的传输。

典型的串口通信使用3根线完成,分别是地线、发送、接收。由于串口通信是异步的,所以端口能够在一根线上发送数据同时在另一根线上接收数据。串口通信最重要的参数是波特率、数据位、停止位和奇偶的校验。对于两个需要进行串口通信的端口,这些参数必须匹配,这也是能够实现串口通讯的前提。

“图1:串行通讯示数据传输意图"
图1:串行通讯示数据传输意图

二、串口通讯的通讯协议?

最初数据是模拟信号输出简单过程量,后来仪表接口出现了RS232接口,这种接口可以实现点对点的通信方式,但这种方式不能实现联网功能,这就促生了RS485。

我们知道串口通信的数据传输都是0和1,在单总线、I2C、UART中都是通过一根线的高低电平来判断逻辑1或者逻辑0,但这种信号线的GND再与其他设备形成共地模式的通信,这种共地模式传输容易产生干扰,并且抗干扰性能也比较弱。所以差分通信、支持多机通信、抗干扰强的RS485就被广泛的使用了。

RS485通信最大特点就是传输速度可以达到10Mb/s以上,传输距离可以达到3000米左右。大家需要注意的是虽然485最大速度和最大传输距离都很大,但是传输的速度是会随距离的增加而变慢的,所以两者是不可以兼得的。

三、串口通讯的物理层

串口通讯的物理层有很多标准,例如上面提到的,我们主要讲解RS-232标准,RS-232标准主要规定了信号的用途、通讯接口以及信号的电平标准。

“精华

在上面的通讯方式中,两个通讯设备的"DB9接口"之间通过串口信号线建立起连接,串口信号线中使用"RS-232标准"传输数据信号。由于RS-232电平标准的信号不能直接被控制器直接识别,所以这些信号会经过一个"电平转换芯片"转换成控制器能识别的"TTL校准"的电平信号,才能实现通讯。

下图为DB9标准串口通讯接口:

“精华

DB9引脚说明:

“精华

上表中的是计算机端的DB9公头标准接法,由于两个通讯设备之间的收发信号(RXD与TXD)应交叉相连,所以调制调解器端的DB9母头的收发信号接法一般与公头的相反,两个设备之间连接时,只要使用"直通型"的串口线连接起来即可。

“精华

串口线中的RTS、CTS、DSR、DTR及DCD信号,使用逻辑 1表示信号有效,逻辑0表示信号无效。例如,当计算机端控制DTR信号线表示为逻辑1时,它是为了告知远端的调制调解器,本机已准备好接收数据,0则表示还没准备就绪。

四、波特率

波特率是指数据信号对载波的调制速率,它用单位时间内载波调制状态改变的次数来表示;

“精华

比如波特率为9600bps;代表的就是每秒中传输9600bit,也就是相当于每一秒中划分成了9600等份。

因此,那么每1bit的时间就是1/9600秒=104.1666...us。约0.1ms。既然是9600等份,即每1bit紧接着下一个比特,不存在额外的间隔。两台设备要想实现串口通讯,这收发端设置的波特率必须相同,否则是没办法实现通讯的。

收发波特率一致可以实现通讯:

“精华

收发波特率不一致,导致RX端不能正常接收:

“精华

五、串口通讯的数据结构

“精华

起始位: 起始位必须是持续一个比特时间的逻辑0电平,标志传输一个字符的开始,接收方可用起始位使自己的接收时钟与发送方的数据同步。

数据位: 数据位紧跟在起始位之后,是通信中的真正有效信息。数据位的位数可以由通信双方共同约定。传输数据时先传送字符的低位,后传送字符的高位。

奇偶校验位: 奇偶校验位仅占一位,用于进行奇校验或偶校验,奇偶检验位不是必须有的。如果是奇校验,需要保证传输的数据总共有奇数个逻辑高位;如果是偶校验,需要保证传输的数据总共有偶数个逻辑高位。

停止位: 停止位可以是是1位、1.5位或2位,可以由软件设定。它一定是逻辑1电平,标志着传输一个字符的结束。

空闲位: 空闲位是指从一个字符的停止位结束到下一个字符的起始位开始,表示线路处于空闲状态,必须由高电平来填充。

六、单双工通讯

单工: 数据传输只支持数据在一个方向上传输;

半双工: 允许数据在两个方向上传输,但某一时刻只允许数据在一个方向上传输,实际上是一种切换方向的单工通信,不需要独立的接收端和发送端,两者可合并为一个端口;

全双工: 允许数据同时在两个方向上传输,因此全双工通信是两个单工方式的结合,需要独立的接收端和发送端。

“精华

七、STM32中的串口通讯

STM32串口通信接口有两种,分别是:UART(通用异步收发器)、USART(通用同步异步收发器),对于大容量STM32F10x系列芯片,分别由3个USART和两个UART。

“精华

对于两芯片的间的连接,两个芯片GND共地,同时TXD和RXD交叉连接,这样两个芯片间可进行TTL电平通信。

但如果对于芯片和PC机相连,除了共地条件外,不能使用如上的直接交叉连接,虽然两者都有TXD和RXD引脚,但通常PC机使用的是RS232接口(9针),通常是TXC和RXD经过电平转换得到,故如果要使芯片与PC机的RS232接口直接通信,需要将芯片的输入输出端口也电平转换为RS232类型,再交叉连接,二者的电平标准不同:

单片机的点评标准(TTL电平):+5V表示1,0V表示0;RS232电平标准:+15/+13V表示0,-15/-13表示1。

“精华

因此单片机与PC机进行串口通信应该遵循:在单片机串口与上位机给出的RS232口之间,通过电平转换电路实现TTL电平与RS232电平间的转换。如果使用USB转串口也可以实现串口通讯,USB转串口电路图如下所示。

“精华

来源:嵌入式资讯精选
免责声明:本文为转载文章,转载此文目的在于传递更多信息,版权归原作者所有。本文所用视频、图片、文字如涉及作品版权问题,请联系小编进行处理(联系邮箱:cathy@eetrend.com)。

围观 41

基于STM32环形队列来实现串口接收数据

cathy的头像

说在前面

码代码的应该学数据结构都学过队列。环形队列是队列的一种特殊形式,应用挺广泛的。因为有太多文章关于这方面的内容,理论知识可以看别人的,下面写得挺好的:STM32进阶之串口环形缓冲区实现。

代码实现

环形队列数据结构

typedef struct ringBuff{
    unsigned int in;               //写入的位置
    unsigned int out;              //读出的位置
    unsigned char buffer[RING_BUFF_SIZE];     //数据域
}stRingBuff;

写一字节数据到队列

RX系列MCU UART空闲中断的软件实现,对于RX系列MCU,为了提高运行效率,减少CPU的占用,UART的数据接收往往和DMA功能一起使用,这样可以无需CPU的介入即可完成UART接收寄存器的数据向指定的接收缓存区的数据传送。此时,只有当预设的DMA传送buffer被填满,即DMA的传送计数器变为0时,才会产生中断。而在有些Use case中,客户需要UART接收不定长度的数据包,如果没有UART空闲检测机制,系统不会自动判断此时UART接收一帧数据已经完成。本文给出给出一个解决思路(只针对不能ELC触发定时器的UART通道。可以ELC触发定时器的UART通道可以通过接收数据后使用ELC触发定时器清零,通过定时器是否超时来判断是否完成一帧数据接收)。

01、设置一个定时器通道,在UART数据接收开始时打开(如果知道什么时候需要接收),或者一直打开(不知道数据什么时候会过来)。定时器周期判断接收DMA通道的传送数据计数器寄存器Transfer Count Register DMCRA。

02、DMCRA 的值在接收到一个字节时候就会减一,接收完一帧数据后这个值就不再变化了。

03、通过定时器周期函数去比较这个值有没有变化,可以判定一帧数据是否接收完成。建议定时器每隔1ms(这个值可以由客户的波特率设置的实际情况来定制)检查一次DMCRA寄存器里的值去判断是否接收完成。

请参考如下程序流程示意图。

“RX系列MCU串口+DMA接收数据完成的空闲判断方法"

下面介绍FIT模块示例配置

UART配置:使能收发by DMAC

“RX系列MCU串口+DMA接收数据完成的空闲判断方法"

DMAC配置:接收和发送配置到SCI9,源地址和目标地址此处随意设置,程序中可以调整。

“RX系列MCU串口+DMA接收数据完成的空闲判断方法"

“RX系列MCU串口+DMA接收数据完成的空闲判断方法"

定时器设置:

“RX系列MCU串口+DMA接收数据完成的空闲判断方法"

以下为实际程序代码,本文中列出关键部分(UART接收完PC发送过来的一帧数据后,回传到PC端)。

重新配置DMAC0接收源地址和目的地址:

“RX系列MCU串口+DMA接收数据完成的空闲判断方法"

定时器间隔中断回调函数:

“RX系列MCU串口+DMA接收数据完成的空闲判断方法"

UART使用DMA发送:

“RX系列MCU串口+DMA接收数据完成的空闲判断方法"

主程序:

“RX系列MCU串口+DMA接收数据完成的空闲判断方法"

实际测试结果(‘发’表示该帧数据是PC发给目标MCU的发送帧,‘收’表示该帧数据是MCU传回来的一帧数据):

“RX系列MCU串口+DMA接收数据完成的空闲判断方法"

来源:瑞萨MCU小百科
免责声明:本文为转载文章,转载此文目的在于传递更多信息,版权归原作者所有。本文所用视频、图片、文字如涉及作品版权问题,请联系小编进行处理(联系邮箱:cathy@eetrend.com)。

围观 587

1、前言

直接存储器访问(Direct Memory Access),简称DMA。DMA是CPU一个用于数据从一个地址空间到另一地址空间“搬运”(拷贝)的组件,数据拷贝过程不需CPU干预,数据拷贝结束则通知CPU处理。因此,大量数据拷贝时,使用DMA可以释放CPU资源。DMA数据拷贝过程,典型的有:

  • 内存—>内存,内存间拷贝
  • 外设—>内存,如uart、spi、i2c等总线接收数据过程
  • 内存—>外设,如uart、spi、i2c等总线发送数据过程

2、串口有必要使用DMA吗

串口(uart)是一种低速的串行异步通信,适用于低速通信场景,通常使用的波特率小于或等于115200bps。对于小于或者等于115200bps波特率的,而且数据量不大的通信场景,一般没必要使用DMA,或者说使用DMA并未能充分发挥出DMA的作用。

对于数量大,或者波特率提高时,必须使用DMA以释放CPU资源,因为高波特率可能带来这样的问题:

  • 对于发送,使用循环发送,可能阻塞线程,需要消耗大量CPU资源“搬运”数据,浪费CPU
  • 对于发送,使用中断发送,不会阻塞线程,但需浪费大量中断资源,CPU频繁响应中断;以115200bps波特率,1s传输11520字节,大约69us需响应一次中断,如波特率再提高,将消耗更多CPU资源
  • 对于接收,如仍采用传统的中断模式接收,同样会因为频繁中断导致消耗大量CPU资源

因此,高波特率场景下,串口非常有必要使用DMA。

3、实现方式

“整体设计图"
整体设计图

4、STM32串口使用DMA

关于STM32串口使用DMA,不乏一些开发板例程及网络上一些博主的使用教程。使用步骤、流程、配置基本大同小异,正确性也没什么毛病,但都是一些基本的Demo例子,作为学习过程没问题;实际项目使用缺乏严谨性,数据量大时可能导致数据异常。

测试平台:

  • STM32F030C8T6
  • UART1/UART2
  • DMA1 Channel2—Channel5
  • ST标准库
  • 主频48MHz(外部12MHz晶振)

“一个严谨的STM32串口DMA发送&接收(1.5Mbps波特率)机制"

5、串口DMA接收

5.1 基本流程

“串口接收流程图"
串口接收流程图

5.2 相关配置

关键步骤

【1】初始化串口

【2】使能串口DMA接收模式,使能串口空闲中断

【3】配置DMA参数,使能DMA通道buf半满(传输一半数据)中断、buf溢满(传输数据完成)中断

【1】第一步,DMA先将数据搬运到buf1,搬运完成通知CPU来拷贝buf1数据
【2】第二步,DMA将数据搬运到buf2,与CPU拷贝buf1数据不会冲突
【3】第三步,buf2数据搬运完成,通知CPU来拷贝buf2数据
【4】执行完第三步,DMA返回执行第一步,一直循环

“双缓存DMA数据搬运过程"
双缓存DMA数据搬运过程

STM32F0系列DMA不支持双缓存(以具体型号为准)机制,但提供了一个buf"半满中断",即是数据搬运到buf大小的一半时,可以产生一个中断信号。基于这个机制,我们可以实现双缓存功能,只需将buf空间开辟大一点即可。

【1】第一步,DMA将数据搬运完成buf的前一半时,产生“半满中断”,CPU来拷贝buf前半部分数据
【2】第二步,DMA继续将数据搬运到buf的后半部分,与CPU拷贝buf前半部数据不会冲突
【3】第三步,buf后半部分数据搬运完成,触发“溢满中断”,CPU来拷贝buf后半部分数据
【4】执行完第三步,DMA返回执行第一步,一直循环

“使用半满中断DMA数据搬运过程"
使用半满中断DMA数据搬运过程

UART2 DMA模式接收配置代码如下,与其他外设使用DMA的配置基本一致,留意关键配置:

  • 串口接收,DMA通道工作模式设为连续模式
  • 使能DMA通道接收buf半满中断、溢满(传输完成)中断
  • 启动DMA通道前清空相关状态标识,防止首次传输错乱数据
void bsp_uart2_dmarx_config(uint8_t *mem_addr, uint32_t mem_size)
{
  	DMA_InitTypeDef DMA_InitStructure;
	
	DMA_DeInit(DMA1_Channel5); 
	DMA_Cmd(DMA1_Channel5, DISABLE);
	DMA_InitStructure.DMA_PeripheralBaseAddr 	= (uint32_t)&(USART2->RDR);/* UART2接收数据地址 */
	DMA_InitStructure.DMA_MemoryBaseAddr 		= (uint32_t)mem_addr; /* 接收buf */
	DMA_InitStructure.DMA_DIR 					= DMA_DIR_PeripheralSRC; 	/* 传输方向:外设->内存 */
	DMA_InitStructure.DMA_BufferSize 			= mem_size; /* 接收buf大小 */
	DMA_InitStructure.DMA_PeripheralInc 		= DMA_PeripheralInc_Disable; 
	DMA_InitStructure.DMA_MemoryInc 			= DMA_MemoryInc_Enable; 
	DMA_InitStructure.DMA_PeripheralDataSize 	= DMA_PeripheralDataSize_Byte; 
	DMA_InitStructure.DMA_MemoryDataSize 		= DMA_MemoryDataSize_Byte;
	DMA_InitStructure.DMA_Mode 					= DMA_Mode_Circular; /* 连续模式 */
	DMA_InitStructure.DMA_Priority 				= DMA_Priority_VeryHigh; 
	DMA_InitStructure.DMA_M2M 					= DMA_M2M_Disable; 
	DMA_Init(DMA1_Channel5, &DMA_InitStructure); 
	DMA_ITConfig(DMA1_Channel5, DMA_IT_TC|DMA_IT_HT|DMA_IT_TE, ENABLE);/* 使能DMA半满、溢满、错误中断 */
	DMA_ClearFlag(DMA1_IT_TC5);	/* 清除相关状态标识 */
	DMA_ClearFlag(DMA1_IT_HT5);
	DMA_Cmd(DMA1_Channel5, ENABLE); 
}

DMA 错误中断“DMA_IT_TE”,一般用于前期调试使用,用于检查DMA出现错误的次数,发布软件可以不使能该中断。

5.3 接收处理

基于上述描述机制,DMA方式接收串口数据,有三种中断场景需要CPU去将buf数据拷贝到fifo中,分别是:

  • DMA通道buf溢满(传输完成)场景
  • DMA通道buf半满场景
  • 串口空闲中断场景

前两者场景,前面文章已经描述。串口空闲中断指的是,数据传输完成后,串口监测到一段时间内没有数据进来,则触发产生的中断信号。

5.3 .1 接收数据大小

数据传输过程是随机的,数据大小也是不定的,存在几类情况:

  • 数据刚好是DMA接收buf的整数倍,这是理想的状态
  • 数据量小于DMA接收buf或者小于接收buf的一半,此时会触发串口空闲中断

因此,我们需根据“DMA通道buf大小”、“DMA通道buf剩余空间大小”、“上一次接收的总数据大小”来计算当前接收的数据大小。

/* 获取DMA通道接收buf剩余空间大小 */
uint16_t DMA_GetCurrDataCounter(DMA_Channel_TypeDef* DMAy_Channelx);

DMA通道buf溢满场景计算

接收数据大小 = DMA通道buf大小 - 上一次接收的总数据大小

DMA通道buf溢满中断处理函数:

void uart_dmarx_done_isr(uint8_t uart_id)
{
  	uint16_t recv_size;
	
	recv_size = s_uart_dev[uart_id].dmarx_buf_size - s_uart_dev[uart_id].last_dmarx_size;

	fifo_write(&s_uart_dev[uart_id].rx_fifo, 
				   (const uint8_t *)&(s_uart_dev[uart_id].dmarx_buf[s_uart_dev[uart_id].last_dmarx_size]), recv_size);

	s_uart_dev[uart_id].last_dmarx_size = 0;
}

DMA通道buf半满场景计算

接收数据大小 = DMA通道接收总数据大小 - 上一次接收的总数据大小
DMA通道接收总数据大小 = DMA通道buf大小 - DMA通道buf剩余空间大小

DMA通道buf半满中断处理函数:

void uart_dmarx_half_done_isr(uint8_t uart_id)
{
  	uint16_t recv_total_size;
  	uint16_t recv_size;
	
	if(uart_id == 0)
	{
	  	recv_total_size = s_uart_dev[uart_id].dmarx_buf_size - bsp_uart1_get_dmarx_buf_remain_size();
	}
	else if (uart_id == 1)
	{
		recv_total_size = s_uart_dev[uart_id].dmarx_buf_size - bsp_uart2_get_dmarx_buf_remain_size();
	}
	recv_size = recv_total_size - s_uart_dev[uart_id].last_dmarx_size;
	
	fifo_write(&s_uart_dev[uart_id].rx_fifo, 
				   (const uint8_t *)&(s_uart_dev[uart_id].dmarx_buf[s_uart_dev[uart_id].last_dmarx_size]), recv_size);
	s_uart_dev[uart_id].last_dmarx_size = recv_total_size;/* 记录接收总数据大小 */
}

串口空闲中断场景计算

串口空闲中断场景的接收数据计算与“DMA通道buf半满场景”计算方式是一样的。

串口空闲中断处理函数:

void uart_dmarx_idle_isr(uint8_t uart_id)
{
  	uint16_t recv_total_size;
  	uint16_t recv_size;
	
	if(uart_id == 0)
	{
	  	recv_total_size = s_uart_dev[uart_id].dmarx_buf_size - bsp_uart1_get_dmarx_buf_remain_size();
	}
	else if (uart_id == 1)
	{
		recv_total_size = s_uart_dev[uart_id].dmarx_buf_size - bsp_uart2_get_dmarx_buf_remain_size();
	}
	recv_size = recv_total_size - s_uart_dev[uart_id].last_dmarx_size;
	s_UartTxRxCount[uart_id*2+1] += recv_size;
	fifo_write(&s_uart_dev[uart_id].rx_fifo, 
				   (const uint8_t *)&(s_uart_dev[uart_id].dmarx_buf[s_uart_dev[uart_id].last_dmarx_size]), recv_size);
	s_uart_dev[uart_id].last_dmarx_size = recv_total_size;
}

注:
串口空闲中断处理函数,除了将数据拷贝到串口接收fifo中,还可以增加特殊处理,如作为串口数据传输完成标识、不定长度数据处理等等。

5.3.2 接收数据偏移地址

将有效数据拷贝到fifo中,除了需知道有效数据大小外,还需知道数据存储于DMA 接收buf的偏移地址。有效数据偏移地址只需记录上一次接收的总大小即,可,在DMA通道buf全满中断处理函数将该值清零,因为下一次数据将从buf的开头存储。

在DMA通道buf溢满中断处理函数中将数据偏移地址清零:

void uart_dmarx_done_isr(uint8_t uart_id)
{
 	/* todo */
	s_uart_dev[uart_id].last_dmarx_size = 0;
}

5.4 应用读取串口数据方法

经过前面的处理步骤,已将串口数据拷贝至接收fifo,应用程序任务只需从fifo获取数据进行处理。前提是,处理效率必须大于DAM接收搬运数据的效率,否则导致数据丢失或者被覆盖处理。

6、串口DMA发送

6.1 基本流程

“串口发送流程图"
串口发送流程图

5.2 相关配置

关键步骤

【1】初始化串口

【2】使能串口DMA发送模式

【3】配置DMA发送通道,这一步无需在初始化设置,有数据需要发送时才配置使能DMA发送通道

UART2 DMA模式发送配置代码如下,与其他外设使用DMA的配置基本一致,留意关键配置:

  • 串口发送是,DMA通道工作模式设为单次模式(正常模式),每次需要发送数据时重新配置DMA
  • 使能DMA通道传输完成中断,利用该中断信息处理一些必要的任务,如清空发送状态、启动下一次传输
  • 启动DMA通道前清空相关状态标识,防止首次传输错乱数据
void bsp_uart2_dmatx_config(uint8_t *mem_addr, uint32_t mem_size)
{
  	DMA_InitTypeDef DMA_InitStructure;
	
	DMA_DeInit(DMA1_Channel4);
	DMA_Cmd(DMA1_Channel4, DISABLE);
	DMA_InitStructure.DMA_PeripheralBaseAddr 	= (uint32_t)&(USART2->TDR);/* UART2发送数据地址 */
	DMA_InitStructure.DMA_MemoryBaseAddr 		= (uint32_t)mem_addr; 	/* 发送数据buf */
	DMA_InitStructure.DMA_DIR 					= DMA_DIR_PeripheralDST; 	/* 传输方向:内存->外设 */
	DMA_InitStructure.DMA_BufferSize 			= mem_size; 			/* 发送数据buf大小 */
	DMA_InitStructure.DMA_PeripheralInc 		= DMA_PeripheralInc_Disable; 
	DMA_InitStructure.DMA_MemoryInc 			= DMA_MemoryInc_Enable; 
	DMA_InitStructure.DMA_PeripheralDataSize 	= DMA_PeripheralDataSize_Byte; 
	DMA_InitStructure.DMA_MemoryDataSize 		= DMA_MemoryDataSize_Byte;
	DMA_InitStructure.DMA_Mode 					= DMA_Mode_Normal; 		/* 单次模式 */
	DMA_InitStructure.DMA_Priority 				= DMA_Priority_High;	 
	DMA_InitStructure.DMA_M2M 					= DMA_M2M_Disable; 
	DMA_Init(DMA1_Channel4, &DMA_InitStructure);  
	DMA_ITConfig(DMA1_Channel4, DMA_IT_TC|DMA_IT_TE, ENABLE); /* 使能传输完成中断、错误中断 */
	DMA_ClearFlag(DMA1_IT_TC4);	/* 清除发送完成标识 */
	DMA_Cmd(DMA1_Channel4, ENABLE); /* 启动DMA发送 */
}

5.3 发送处理

串口待发送数据存于发送fifo中,发送处理函数需要做的的任务就是循环查询发送fifo是否存在数据,如存在则将该数据拷贝到DMA发送buf中,然后启动DMA传输。前提是需要等待上一次DMA传输完毕,即是DMA接收到DMA传输完成中断信号"DMA_IT_TC"。

串口发送处理函数:

void uart_poll_dma_tx(uint8_t uart_id)
{
  	uint16_t size = 0;
	
	if (0x01 == s_uart_dev[uart_id].status)
    {
        return;
    }
	size = fifo_read(&s_uart_dev[uart_id].tx_fifo, s_uart_dev[uart_id].dmatx_buf,
					 s_uart_dev[uart_id].dmatx_buf_size);
	if (size != 0)
	{
        s_UartTxRxCount[uart_id*2+0] += size;
	  	if (uart_id == 0)
		{
            s_uart_dev[uart_id].status = 0x01;	/* DMA发送状态 */
		  	bsp_uart1_dmatx_config(s_uart_dev[uart_id].dmatx_buf, size);
		}
		else if (uart_id == 1)
		{
            s_uart_dev[uart_id].status = 0x01;	/* DMA发送状态,必须在使能DMA传输前置位,否则有可能DMA已经传输并进入中断 */
			bsp_uart2_dmatx_config(s_uart_dev[uart_id].dmatx_buf, size);
		}
	}
}

注意发送状态标识,必须先置为“发送状态”,然后启动DMA 传输。如果步骤反过来,在传输数据量少时,DMA传输时间短,“DMA_IT_TC”中断可能比“发送状态标识置位”先执行,导致程序误判DMA一直处理发送状态(发送标识无法被清除)。

注:
关于DMA发送数据启动函数,有些博客文章描述只需改变DMA发送buf的大小即可;经过测试发现,该方法在发送数据量较小时可行,数据量大后,导致发送失败,而且不会触发DMA发送完成中断。因此,可靠办法是:每次启动DMA发送,重新配置DMA通道所有参数。该步骤只是配置寄存器过程,实质上不会占用很多CPU执行时间。

DMA传输完成中断处理函数:

void uart_dmatx_done_isr(uint8_t uart_id)
{
 	s_uart_dev[uart_id].status = 0;	/* 清空DMA发送状态标识 */
}

上述串口发送处理函数可以在几种情况调用:

主线程任务调用,前提是线程不能被其他任务阻塞,否则导致fifo溢出

void thread(void)
{
    uart_poll_dma_tx(DEV_UART1);
    uart_poll_dma_tx(DEV_UART2);
}

定时器中断中调用

void TIMx_IRQHandler(void)
{
    uart_poll_dma_tx(DEV_UART1);
    uart_poll_dma_tx(DEV_UART2);
}

DMA通道传输完成中断中调用

void DMA1_Channel4_5_IRQHandler(void)
{
	if(DMA_GetITStatus(DMA1_IT_TC4))
	{
		UartDmaSendDoneIsr(UART_2);
		DMA_ClearFlag(DMA1_FLAG_TC4);
		uart_poll_dma_tx(DEV_UART2);
	}
}

每次拷贝多少数据量到DMA发送buf:

关于这个问题,与具体应用场景有关,遵循的原则就是:只要发送fifo的数据量大于等于DMA发送buf的大小,就应该填满DMA发送buf,然后启动DMA传输,这样才能充分发挥会DMA性能。因此,需兼顾每次DMA传输的效率和串口数据流实时性,参考着几类实现:

  • 周期查询发送fifo数据,启动DMA传输,充分利用DMA发送效率,但可能降低串口数据流实时性
  • 实时查询发送fifo数据,加上超时处理,理想的方法
  • 在DMA传输完成中断中处理,保证实时连续数据流

6、串口设备

6.1 数据结构

/* 串口设备数据结构 */
typedef struct
{
	uint8_t status;			/* 发送状态 */
	_fifo_t tx_fifo;		/* 发送fifo */
	_fifo_t rx_fifo;		/* 接收fifo */
	uint8_t *dmarx_buf;		/* dma接收缓存 */
	uint16_t dmarx_buf_size;/* dma接收缓存大小*/
	uint8_t *dmatx_buf;		/* dma发送缓存 */
	uint16_t dmatx_buf_size;/* dma发送缓存大小 */
	uint16_t last_dmarx_size;/* dma上一次接收数据大小 */
}uart_device_t;

6.2 对外接口

/* 串口注册初始化函数 */
void uart_device_init(uint8_t uart_id)
{
  	if (uart_id == 1)
	{
		/* 配置串口2收发fifo */
		fifo_register(&s_uart_dev[uart_id].tx_fifo, &s_uart2_tx_buf[0], 
                      sizeof(s_uart2_tx_buf), fifo_lock, fifo_unlock);
		fifo_register(&s_uart_dev[uart_id].rx_fifo, &s_uart2_rx_buf[0], 
                      sizeof(s_uart2_rx_buf), fifo_lock, fifo_unlock);
		
		/* 配置串口2 DMA收发buf */
		s_uart_dev[uart_id].dmarx_buf = &s_uart2_dmarx_buf[0];
		s_uart_dev[uart_id].dmarx_buf_size = sizeof(s_uart2_dmarx_buf);
		s_uart_dev[uart_id].dmatx_buf = &s_uart2_dmatx_buf[0];
		s_uart_dev[uart_id].dmatx_buf_size = sizeof(s_uart2_dmatx_buf);
		bsp_uart2_dmarx_config(s_uart_dev[uart_id].dmarx_buf, 
							   sizeof(s_uart2_dmarx_buf));
		s_uart_dev[uart_id].status  = 0;
	}
}

/* 串口发送函数 */
uint16_t uart_write(uint8_t uart_id, const uint8_t *buf, uint16_t size)
{
	return fifo_write(&s_uart_dev[uart_id].tx_fifo, buf, size);
}

/* 串口读取函数 */
uint16_t uart_read(uint8_t uart_id, uint8_t *buf, uint16_t size)
{
	return fifo_read(&s_uart_dev[uart_id].rx_fifo, buf, size);
}

7、相关文章

依赖的fifo参考该文章:

【1】通用环形缓冲区模块

8、完整源码

代码仓库:https://github.com/Prry/stm32f0-uart-dma

串口&DMA底层配置:

#include <stddef.h>
#include <stdint.h>
#include <stdbool.h>
#include "stm32f0xx.h"
#include "bsp_uart.h"

/**
 * @brief  
 * @param  
 * @retval 
 */
static void bsp_uart1_gpio_init(void)
{
    GPIO_InitTypeDef    GPIO_InitStructure;
#if 0
	RCC_AHBPeriphClockCmd(RCC_AHBPeriph_GPIOB, ENABLE);
	
    GPIO_PinAFConfig(GPIOB, GPIO_PinSource6, GPIO_AF_0);
    GPIO_PinAFConfig(GPIOB, GPIO_PinSource7, GPIO_AF_0); 
	
	GPIO_InitStructure.GPIO_Pin 	= GPIO_Pin_6 | GPIO_Pin_7;
    GPIO_InitStructure.GPIO_Mode 	= GPIO_Mode_AF;
	GPIO_InitStructure.GPIO_OType 	= GPIO_OType_PP;
    GPIO_InitStructure.GPIO_Speed  	= GPIO_Speed_Level_3;
    GPIO_InitStructure.GPIO_PuPd 	= GPIO_PuPd_UP;
    GPIO_Init(GPIOB, &GPIO_InitStructure);
#else
	RCC_AHBPeriphClockCmd(RCC_AHBPeriph_GPIOA, ENABLE);
	
    GPIO_PinAFConfig(GPIOB, GPIO_PinSource9, GPIO_AF_1);
    GPIO_PinAFConfig(GPIOB, GPIO_PinSource10, GPIO_AF_1); 
	
	GPIO_InitStructure.GPIO_Pin 	= GPIO_Pin_9 | GPIO_Pin_10;
    GPIO_InitStructure.GPIO_Mode 	= GPIO_Mode_AF;
	GPIO_InitStructure.GPIO_OType 	= GPIO_OType_PP;
    GPIO_InitStructure.GPIO_Speed  	= GPIO_Speed_Level_3;
    GPIO_InitStructure.GPIO_PuPd 	= GPIO_PuPd_UP;
    GPIO_Init(GPIOA, &GPIO_InitStructure);
#endif
}

/**
 * @brief  
 * @param  
 * @retval 
 */
static void bsp_uart2_gpio_init(void)
{
	GPIO_InitTypeDef GPIO_InitStructure;
	
	RCC_AHBPeriphClockCmd(RCC_AHBPeriph_GPIOB, ENABLE);
	
	GPIO_PinAFConfig(GPIOA, GPIO_PinSource2, GPIO_AF_1);
	GPIO_PinAFConfig(GPIOA, GPIO_PinSource3, GPIO_AF_1);
	
	GPIO_InitStructure.GPIO_Pin   = GPIO_Pin_2 | GPIO_Pin_3;
	GPIO_InitStructure.GPIO_Mode  = GPIO_Mode_AF;
	GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_10MHz;
	GPIO_InitStructure.GPIO_PuPd  = GPIO_PuPd_UP;
	GPIO_Init(GPIOA, &GPIO_InitStructure);
}

/**
 * @brief  
 * @param  
 * @retval 
 */
void bsp_uart1_init(void)
{
	USART_InitTypeDef USART_InitStructure;
	NVIC_InitTypeDef NVIC_InitStructure;
	
	bsp_uart1_gpio_init();
	
	/* 使能串口和DMA时钟 */
	RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);
	
	USART_InitStructure.USART_BaudRate            = 57600;
	USART_InitStructure.USART_WordLength          = USART_WordLength_8b;
	USART_InitStructure.USART_StopBits            = USART_StopBits_1;
	USART_InitStructure.USART_Parity              = USART_Parity_No;
	USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
	USART_InitStructure.USART_Mode                = USART_Mode_Rx | USART_Mode_Tx;
	USART_Init(USART1, &USART_InitStructure);
	
	USART_ITConfig(USART1, USART_IT_IDLE, ENABLE);	/* 使能空闲中断 */
	USART_OverrunDetectionConfig(USART1, USART_OVRDetection_Disable);
	
	USART_Cmd(USART1, ENABLE);
	USART_DMACmd(USART1, USART_DMAReq_Rx|USART_DMAReq_Tx, ENABLE); /* 使能DMA收发 */

	/* 串口中断 */
	NVIC_InitStructure.NVIC_IRQChannel         = USART1_IRQn;
	NVIC_InitStructure.NVIC_IRQChannelPriority = 2;
	NVIC_InitStructure.NVIC_IRQChannelCmd      = ENABLE;
	NVIC_Init(&NVIC_InitStructure);

	/* DMA中断 */
  	NVIC_InitStructure.NVIC_IRQChannel 		   = DMA1_Channel2_3_IRQn;       
  	NVIC_InitStructure.NVIC_IRQChannelPriority = 0; 
	NVIC_InitStructure.NVIC_IRQChannelCmd      = ENABLE;
  	NVIC_Init(&NVIC_InitStructure);
}

/**
 * @brief  
 * @param  
 * @retval 
 */
void bsp_uart2_init(void)
{
	USART_InitTypeDef USART_InitStructure;
	NVIC_InitTypeDef NVIC_InitStructure;
	
	bsp_uart2_gpio_init();
	
	/* 使能串口和DMA时钟 */
	RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2, ENABLE);

	USART_InitStructure.USART_BaudRate            = 57600;
	USART_InitStructure.USART_WordLength          = USART_WordLength_8b;
	USART_InitStructure.USART_StopBits            = USART_StopBits_1;
	USART_InitStructure.USART_Parity              = USART_Parity_No;
	USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
	USART_InitStructure.USART_Mode                = USART_Mode_Rx | USART_Mode_Tx;
	USART_Init(USART2, &USART_InitStructure);
	
	USART_ITConfig(USART2, USART_IT_IDLE, ENABLE);	/* 使能空闲中断 */
	USART_OverrunDetectionConfig(USART2, USART_OVRDetection_Disable);
	
	USART_Cmd(USART2, ENABLE);
	USART_DMACmd(USART2, USART_DMAReq_Rx|USART_DMAReq_Tx, ENABLE); 	/* 使能DMA收发 */

	/* 串口中断 */
	NVIC_InitStructure.NVIC_IRQChannel         = USART2_IRQn;
	NVIC_InitStructure.NVIC_IRQChannelPriority = 2;
	NVIC_InitStructure.NVIC_IRQChannelCmd      = ENABLE;
	NVIC_Init(&NVIC_InitStructure);

	/* DMA中断 */
	NVIC_InitStructure.NVIC_IRQChannel         = DMA1_Channel4_5_IRQn;       
  	NVIC_InitStructure.NVIC_IRQChannelPriority = 0; 
	NVIC_InitStructure.NVIC_IRQChannelCmd      = ENABLE;
  	NVIC_Init(&NVIC_InitStructure);
}

void bsp_uart1_dmatx_config(uint8_t *mem_addr, uint32_t mem_size)
{
  	DMA_InitTypeDef DMA_InitStructure;
	
	DMA_DeInit(DMA1_Channel2);
	DMA_Cmd(DMA1_Channel2, DISABLE);
	DMA_InitStructure.DMA_PeripheralBaseAddr 	= (uint32_t)&(USART1->TDR);
	DMA_InitStructure.DMA_MemoryBaseAddr 		= (uint32_t)mem_addr; 
	DMA_InitStructure.DMA_DIR 					= DMA_DIR_PeripheralDST; 	/* 传输方向:内存->外设 */
	DMA_InitStructure.DMA_BufferSize 			= mem_size; 
	DMA_InitStructure.DMA_PeripheralInc 		= DMA_PeripheralInc_Disable; 
	DMA_InitStructure.DMA_MemoryInc 			= DMA_MemoryInc_Enable; 
	DMA_InitStructure.DMA_PeripheralDataSize 	= DMA_PeripheralDataSize_Byte; 
	DMA_InitStructure.DMA_MemoryDataSize 		= DMA_MemoryDataSize_Byte;
	DMA_InitStructure.DMA_Mode 					= DMA_Mode_Normal; 
	DMA_InitStructure.DMA_Priority 				= DMA_Priority_High; 
	DMA_InitStructure.DMA_M2M 					= DMA_M2M_Disable; 
	DMA_Init(DMA1_Channel2, &DMA_InitStructure);  
	DMA_ITConfig(DMA1_Channel2, DMA_IT_TC|DMA_IT_TE, ENABLE); 
	DMA_ClearFlag(DMA1_IT_TC2);	/* 清除发送完成标识 */
	DMA_Cmd(DMA1_Channel2, ENABLE); 
}

void bsp_uart1_dmarx_config(uint8_t *mem_addr, uint32_t mem_size)
{
  	DMA_InitTypeDef DMA_InitStructure;
	
	DMA_DeInit(DMA1_Channel3); 
	DMA_Cmd(DMA1_Channel3, DISABLE);
	DMA_InitStructure.DMA_PeripheralBaseAddr 	= (uint32_t)&(USART1->RDR);
	DMA_InitStructure.DMA_MemoryBaseAddr 		= (uint32_t)mem_addr; 
	DMA_InitStructure.DMA_DIR 					= DMA_DIR_PeripheralSRC; 	/* 传输方向:外设->内存 */
	DMA_InitStructure.DMA_BufferSize 			= mem_size; 
	DMA_InitStructure.DMA_PeripheralInc 		= DMA_PeripheralInc_Disable; 
	DMA_InitStructure.DMA_MemoryInc 			= DMA_MemoryInc_Enable; 
	DMA_InitStructure.DMA_PeripheralDataSize 	= DMA_PeripheralDataSize_Byte; 
	DMA_InitStructure.DMA_MemoryDataSize 		= DMA_MemoryDataSize_Byte;
	DMA_InitStructure.DMA_Mode 					= DMA_Mode_Circular; 
	DMA_InitStructure.DMA_Priority 				= DMA_Priority_VeryHigh; 
	DMA_InitStructure.DMA_M2M 					= DMA_M2M_Disable; 
	DMA_Init(DMA1_Channel3, &DMA_InitStructure); 
	DMA_ITConfig(DMA1_Channel3, DMA_IT_TC|DMA_IT_HT|DMA_IT_TE, ENABLE);/* 使能DMA半满、全满、错误中断 */
	DMA_ClearFlag(DMA1_IT_TC3);
	DMA_ClearFlag(DMA1_IT_HT3);
	DMA_Cmd(DMA1_Channel3, ENABLE); 
}

uint16_t bsp_uart1_get_dmarx_buf_remain_size(void)
{
	return DMA_GetCurrDataCounter(DMA1_Channel3);	/* 获取DMA接收buf剩余空间 */
}

void bsp_uart2_dmatx_config(uint8_t *mem_addr, uint32_t mem_size)
{
  	DMA_InitTypeDef DMA_InitStructure;
	
	DMA_DeInit(DMA1_Channel4);
	DMA_Cmd(DMA1_Channel4, DISABLE);
	DMA_InitStructure.DMA_PeripheralBaseAddr 	= (uint32_t)&(USART2->TDR);
	DMA_InitStructure.DMA_MemoryBaseAddr 		= (uint32_t)mem_addr; 
	DMA_InitStructure.DMA_DIR 					= DMA_DIR_PeripheralDST; 	/* 传输方向:内存->外设 */
	DMA_InitStructure.DMA_BufferSize 			= mem_size; 
	DMA_InitStructure.DMA_PeripheralInc 		= DMA_PeripheralInc_Disable; 
	DMA_InitStructure.DMA_MemoryInc 			= DMA_MemoryInc_Enable; 
	DMA_InitStructure.DMA_PeripheralDataSize 	= DMA_PeripheralDataSize_Byte; 
	DMA_InitStructure.DMA_MemoryDataSize 		= DMA_MemoryDataSize_Byte;
	DMA_InitStructure.DMA_Mode 					= DMA_Mode_Normal; 
	DMA_InitStructure.DMA_Priority 				= DMA_Priority_High; 
	DMA_InitStructure.DMA_M2M 					= DMA_M2M_Disable; 
	DMA_Init(DMA1_Channel4, &DMA_InitStructure);  
	DMA_ITConfig(DMA1_Channel4, DMA_IT_TC|DMA_IT_TE, ENABLE); 
	DMA_ClearFlag(DMA1_IT_TC4);	/* 清除发送完成标识 */
	DMA_Cmd(DMA1_Channel4, ENABLE); 
}

void bsp_uart2_dmarx_config(uint8_t *mem_addr, uint32_t mem_size)
{
  	DMA_InitTypeDef DMA_InitStructure;
	
	DMA_DeInit(DMA1_Channel5); 
	DMA_Cmd(DMA1_Channel5, DISABLE);
	DMA_InitStructure.DMA_PeripheralBaseAddr 	= (uint32_t)&(USART2->RDR);
	DMA_InitStructure.DMA_MemoryBaseAddr 		= (uint32_t)mem_addr; 
	DMA_InitStructure.DMA_DIR 					= DMA_DIR_PeripheralSRC; 	/* 传输方向:外设->内存 */
	DMA_InitStructure.DMA_BufferSize 			= mem_size; 
	DMA_InitStructure.DMA_PeripheralInc 		= DMA_PeripheralInc_Disable; 
	DMA_InitStructure.DMA_MemoryInc 			= DMA_MemoryInc_Enable; 
	DMA_InitStructure.DMA_PeripheralDataSize 	= DMA_PeripheralDataSize_Byte; 
	DMA_InitStructure.DMA_MemoryDataSize 		= DMA_MemoryDataSize_Byte;
	DMA_InitStructure.DMA_Mode 					= DMA_Mode_Circular; 
	DMA_InitStructure.DMA_Priority 				= DMA_Priority_VeryHigh; 
	DMA_InitStructure.DMA_M2M 					= DMA_M2M_Disable; 
	DMA_Init(DMA1_Channel5, &DMA_InitStructure); 
	DMA_ITConfig(DMA1_Channel5, DMA_IT_TC|DMA_IT_HT|DMA_IT_TE, ENABLE);/* 使能DMA半满、全满、错误中断 */
	DMA_ClearFlag(DMA1_IT_TC5);
	DMA_ClearFlag(DMA1_IT_HT5);
	DMA_Cmd(DMA1_Channel5, ENABLE); 
}

uint16_t bsp_uart2_get_dmarx_buf_remain_size(void)
{
	return DMA_GetCurrDataCounter(DMA1_Channel5);	/* 获取DMA接收buf剩余空间 */
}

压力测试:

  • 1.5Mbps波特率,串口助手每毫秒发送1k字节数据,stm32f0 DMA接收数据,再通过DMA发送回串口助手,毫无压力。
  • 1.5Mbps波特率,可传输大文件测试,将接收数据保存为文件,与源文件比较。
  • 串口高波特率测试需要USB转TLL工具及串口助手都支持才可行,推荐CP2102、FT232芯片的USB转TTL工具。

“1.5Mbps串口回环压力测试"
1.5Mbps串口回环压力测试

————————————————

版权声明:本文为CSDN博主「Acuity.」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_20553613/article/details/108367512
免责声明:本文为转载文章,转载此文目的在于传递更多信息,版权归原作者所有。本文所用视频、图片、文字如涉及作品版权问题,请联系小编进行处理(联系邮箱:cathy@eetrend.com)。

围观 868

今天再给大家分享一些关于STM32串口中断及DMA接收常见的几个问题。

UART串口中断接收

使能UART串口中断之后,有接收到UART数据,进入中断,此时要清除RXNE接收标志位:

1)通过软件向该RXNE标志位写入零来清零;

2)通过对 USART_DR 寄存器执行读入操作将该位清零。

这里可以查看对应《参考手册》,一般我们选择第2种,通过读取UART串口数据来清零。

1、中断接收数据丢失

在UART串口中断函数中,或者更高优先级中断函数中长时间执行,导致接收丢失,所以,请勿在中断函数中长时间执行。

特别有些人,还在中断函数添加延时函数。实际应用中,只要不是特殊情况,比如测试某个功能可以添加延时函数,都不建议在中断函数添加延时函数。

2、ORE上溢错误

ORE上溢错误是什么意思呢? 可能很多人不了解,简单说就是:UART接收到有数据,没有去取,但又来了数据,此时就会产生ORE上溢错误。(请看“参考手册”)

其实,上面这种长时间在占用中断,就会导致UART接收数据上溢。

很多人没有在意这个,如果是使能了中断接收,标志位没有清除,又有ORE上溢错误的话,程序就会不停地进入UART中断。(大家不妨试一下,看一下是不是你代码一直在UART中断里面不停运行)。

3、使能接收中断前,先清除接收标志位

有时候,在程序初始化的时候,就会接收到数据,这个时候建议大家先清除接收标志位再使能接收中断。类似如下:

“STM32串口中断及DMA接收常见的几个问题"

DMA接收串口数据

使用DMA接收串口数据,相信很多朋友都知道。这个可以理解为使用队列,或者FIFO的形式,防止因高优先级中断而打断,导致接收数据丢失。

但很多人都遇到过,接收不到数据,或者数据异常的情况。所以,这里同样有需要注意的地方。

1、使能UART之前,先使能DMA相关配置

这个和清除标志位一样的道理,使能UART之前,先使能DMA相关配置,防止在配置过程中有接收数据。

“STM32串口中断及DMA接收常见的几个问题"

2、ORE上溢错误导致不能使用DMA接收数据

导致不能DMA接收不到数据的根源,有一种可能就是有ORE上溢错误。

若在串口初始化并使能后到 DMA 使能之前有数据来,MCU是不能接收的。如果此时数据寄存器 USART_DR 存在一个数据,再来一个数据,就会导致ORE上溢错误。

一旦产生上溢错误后,就无法再触发 DAM 请求,即使之后再启动 DMA 也不行,无法触发 DMA 请求就无法将数据寄存器内的数据及时转移走,如此陷入死锁。这就是串口无法正常接收的原因。

所以,最后提醒大家,配置时,请一定要注意这些细节。

来源:嵌入式专栏(作者 | strongerHuang)
免责声明:本文为转载文章,转载此文目的在于传递更多信息,版权归原作者所有。本文所用视频、图片、文字如涉及作品版权问题,请联系小编进行处理(联系邮箱:cathy@eetrend.com)。

围观 4241

串口发送数据

1、串口发送数据最直接的方式就是标准调用库函数

void USART_SendData(USART_TypeDef* USARTx, uint16_t Data);

第一个参数是发送的串口号,第二个参数是要发送的数据了。但是用过的朋友应该觉得不好用,一次只能发送单个字符,所以我们有必要根据这个函数加以扩展:

void Send_data(u8 *s)
{
while(*s!='\0')
 { 
while(USART_GetFlagStatus(USART1,USART_FLAG_TC )==RESET); 
  USART_SendData(USART1,*s);
  s++;
 }
}

以上程序的形参就是我们调用该函数时要发送的字符串,这里通过循环调用USART_SendData来一 一发送我们的字符串。

while(USART_GetFlagStatus(USART1,USART_FLAG_TC )==RESET);

这句话有必要加,他是用于检查串口是否发送完成的标志,如果不加这句话会发生数据丢失的情况。这个函数只能用于串口1发送。有些时候根据需要,要用到多个串口发送那么就还需要改进这个程序。如下:

void Send_data(USART_TypeDef * USARTx,u8 *s)
{
while(*s!='\0')
 { 
while(USART_GetFlagStatus(USARTx,USART_FLAG_TC )==RESET); 
  USART_SendData(USARTx,*s);
  s++;
 }
}

这样就可实现任意的串口发送。但有一点,我在使用实时操作系统如UCOS,Freertos等的时候,需考虑函数重入的问题,相关推荐:使用STM32CubeMx工具,写FreeRTOS的demo程序

当然也可以简单的实现把该函数复制一下,然后修改串口号也可以避免该问题。然而这个函数不能像printf那样传递多个参数,所以还可以在改进,最终程序如下:

void USART_printf ( USART_TypeDef * USARTx, char * Data, ... )
{
const char *s;
int d;   
char buf[16];

 va_list ap;
 va_start(ap, Data);

while ( * Data != 0 )     // 判断是否到达字符串结束符
 {                              
if ( * Data == 0x5c )  //'\'
  {           
switch ( *++Data )
   {
case 'r':                 //回车符
    USART_SendData(USARTx, 0x0d);
    Data ++;
break;

case 'n':                 //换行符
    USART_SendData(USARTx, 0x0a); 
    Data ++;
break;

default:
    Data ++;
break;
   }    
  }

else if ( * Data == '%')
  {           //
switch ( *++Data )
   {    
case 's':            //字符串
    s = va_arg(ap, const char *);

for ( ; *s; s++) 
    {
     USART_SendData(USARTx,*s);
while( USART_GetFlagStatus(USARTx, USART_FLAG_TXE) == RESET );
    }

    Data++;

break;

case 'd':   
//十进制
    d = va_arg(ap, int);

    itoa(d, buf, 10);

for (s = buf; *s; s++) 
    {
     USART_SendData(USARTx,*s);
while( USART_GetFlagStatus(USARTx, USART_FLAG_TXE) == RESET );
    }

    Data++;

break;

default:
    Data++;

break;

   }   
  }

else USART_SendData(USARTx, *Data++);

while ( USART_GetFlagStatus ( USARTx, USART_FLAG_TXE ) == RESET );

 }
}

该函数就可以像printf使用可变参数,方便很多。通过观察函数但这个函数只支持了%d,%s的参数,想要支持更多,可以仿照printf的函数写法加以补充。

2、直接使用printf函数

很多朋友都知道想要STM32要直接使用printf不行的。需要加上以下的重映射函数:

“STM32串口发送数据和接收数据方式总结"

如果不想添加以上代码,也可以勾选以下的Use MicroLI选项来支持printf函数使用:

“STM32串口发送数据和接收数据方式总结"

串口接收数据

串口接收最后应有一定的协议,如发送一帧数据应该有头标志或尾标志,也可两个标志都有,串口其他相关文章:学习STM32单片机,绕不开的串口。

这样在处理数据时既能能保证数据的正确接收,也有利于接收完后我们处理数据。串口的配置在这里就不在赘述,这里我以串口2接收中断服务程序函数且接收的数据包含头尾标识为例。

#define Max_BUFF_Len 18
unsigned char Uart2_Buffer[Max_BUFF_Len];
unsigned int Uart2_Rx=0;
void USART2_IRQHandler()
{
if(USART_GetITStatus(USART2,USART_IT_RXNE) != RESET) //中断产生 
 {
  USART_ClearITPendingBit(USART2,USART_IT_RXNE); //清除中断标志

  Uart2_Buffer[Uart2_Rx] = USART_ReceiveData(USART2);     //接收串口1数据到buff缓冲区
  Uart2_Rx++; 

if(Uart2_Buffer[Uart2_Rx-1] == 0x0a || Uart2_Rx == Max_BUFF_Len)    //如果接收到尾标识是换行符(或者等于最大接受数就清空重新接收)
  {
if(Uart2_Buffer[0] == '+')                      //检测到头标识是我们需要的 
   {
printf("%s\r\n",Uart2_Buffer);        //这里我做打印数据处理
    Uart2_Rx=0;                                   
   } 
else
   {
    Uart2_Rx=0;                                   //不是我们需要的数据或者达到最大接收数则开始重新接收
   }
  }
 }
}

数据的头标识为“\n”,即换行符,尾标识为“+”。该函数将串口接收的数据存放在USART_Buffer数组中,然后先判断当前字符是不是尾标识,如果是说明接收完毕,然后再来判断头标识是不是“+”号,如果还是那么就是我们想要的数据,接下来就可以进行相应数据的处理了。但如果不是那么就让Usart2_Rx=0重新接收数据。

这样做的有以下好处:

  • 可以接受不定长度的数据,最大接收长度可以通过Max_BUFF_Len来更改

  • 可以接受指定的数据

  • 防止接收的数据使数组越界

这里我的把接受正确数据直接打印出来,也可以通过设置标识位,然后在主函数里面轮询再操作。

以上的接收形式,是中断一次就接收一个字符,这在UCOS等实时内核系统中频繁的中断,非常消耗CPU资源,在有些时候我们需要接收大量数据时且波特率很高的情况下,长时间中断会带来一些额外的问题。

所以以DMA形式配合串口的IDLE(空闲中断)来接受数据将会大大的提高CPU的利用率,减少系统资源的消耗。首先还是先看代码。

#define DMA_USART1_RECEIVE_LEN 18
void USART1_IRQHandler(void)
{     
    u32 temp = 0;  
uint16_t i = 0;  

if(USART_GetITStatus(USART1, USART_IT_IDLE) != RESET)  
    {  
        USART1->SR;  
        USART1->DR; //这里我们通过先读SR(状态寄存器)和DR(数据寄存器)来清USART_IT_IDLE标志    
        DMA_Cmd(DMA1_Channel5,DISABLE);  
        temp = DMA_USART1_RECEIVE_LEN - DMA_GetCurrDataCounter(DMA1_Channel5); //接收的字符串长度=设置的接收长度-剩余DMA缓存大小 
for (i = 0;i < temp;i++)  
        {  
            Uart2_Buffer[i] = USART1_RECEIVE_DMABuffer[i];  

        }  
//设置传输数据长度  
        DMA_SetCurrDataCounter(DMA1_Channel5,DMA_USART1_RECEIVE_LEN);  
//打开DMA  
        DMA_Cmd(DMA1_Channel5,ENABLE);  
    }        
} 

之前的串口中断是一个一个字符的接收,现在改为串口空闲中断,就是一帧数据过来才中断进入一次。而且接收的数据时候是DMA来搬运到我们指定的缓冲区(也就是程序中的USART1_RECEIVE_DMABuffer数组),是不占用CPU时间资源的。

最后在讲下DMA的发送:

#define DMA_USART1_SEND_LEN 64
void DMA_SEND_EN(void)
{
 DMA_Cmd(DMA1_Channel4, DISABLE);      
 DMA_SetCurrDataCounter(DMA1_Channel4,DMA_USART1_SEND_LEN);   
 DMA_Cmd(DMA1_Channel4, ENABLE);
}

这里需要注意下DMA_Cmd(DMA1_Channel4,DISABLE)函数需要在设置传输大小之前调用一下,否则不会重新启动DMA发送。

有了以上的接收方式,对一般的串口数据处理是没有问题的了。下面再讲一下,在ucosiii中我使用信号量+消息队列+储存管理的形式来处理我们的串口数据。先来说一下这种方式对比其他方式的一些优缺点。

一般对串口的处理形式是"生产者"和"消费者"的模式,即本次接收的数据要马上处理,否则当数据大量涌进的时候,就来不及"消费"掉生产者(串口接收中断)的数据,那么就会丢失本次的数据处理。所以使用队列就能够很方便的解决这个问题。

在下面的程序中,对数据的处理是先接受,在处理,如果在处理的过程中,有串口中断接受数据,那么就把它依次放在队列中,队列的特征是先进先出,在串口中就是先处理先接受的数据,所以根据生产和消费的速度,定义不同大小的消息队列缓冲区就可以了。缺点就是太占用系统资源,一般51单片机是没可能了。下面是从我做的项目中截取过来的程序:

OS_MSG_SIZE  Usart1_Rx_cnt;          //字节大小计数值
unsigned char Usart1_data;           //每次中断接收的数据
unsigned char* Usart1_Rx_Ptr;        //储存管理分配内存的首地址的指针
unsigned char* Usart1_Rx_Ptr1;       //储存首地址的指针

void USART1_IRQHandler() 
{
 OS_ERR err;
 OSIntEnter();

  if(USART_GetFlagStatus(USART1,USART_FLAG_RXNE) != RESET) //中断产生 
  {   
    USART_ClearFlag(USART1, USART_FLAG_RXNE);     //清除中断标志

    Usart1_data = USART_ReceiveData(USART1);     //接收串口1数据到buff缓冲区

  if(Usart1_data =='+')                     //接收到数据头标识
  {
//   OSSemPend((OS_SEM*  )&SEM_IAR_UART,  //这里请求信号量是为了保证分配的存储区,但一般来说不允许
//   (OS_TICK  )0,                   //在终端服务函数中调用信号量请求但因为
//   (OS_OPT   )OS_OPT_PEND_NON_BLOCKING,//我OPT参数设置为非阻塞,所以可以这么写
//   (CPU_TS*  )0,
//   (OS_ERR*  )&err); 
//   if(err==OS_ERR_PEND_WOULD_BLOCK)     //检测到当前信号量不可用
//   {
//     printf("error");
//   }    
   Usart1_Rx_Ptr=(unsigned char*) OSMemGet((OS_MEM*)&UART1_MemPool,&err);//分配存储区
   Usart1_Rx_Ptr1=Usart1_Rx_Ptr;          //储存存储区的首地址
  }
  if(Usart1_data == 0x0a )       //接收到尾标志
  {                    
   *Usart1_Rx_Ptr++=Usart1_data;
   Usart1_Rx_cnt++;                         //字节大小增加
   OSTaskQPost((OS_TCB    *  )&Task1_TaskTCB,
                                   (void      *  )Usart1_Rx_Ptr1,    //发送存储区首地址到消息队列
                                   (OS_MSG_SIZE  )Usart1_Rx_cnt,
                                   (OS_OPT       )OS_OPT_POST_FIFO,  //先进先出,也可设置为后进先出,再有地方很有用
                                   (OS_ERR    *  )&err);

   Usart1_Rx_Ptr=NULL;          //将指针指向为空,防止修改
   Usart1_Rx_cnt=0;      //字节大小计数清零
  }
  else
  {
   *Usart1_Rx_Ptr=Usart1_data; //储存接收到的数据
   Usart1_Rx_Ptr++;
   Usart1_Rx_cnt++;
  } 
 }    
 OSIntExit();
}

上面被注释掉的代码为我是为了防止当分区中没有空闲的存储块时加入信号量,打印出报警信息。当然我们也可以将存储块直接设置大一点,但是还是无法避免当没有可有存储块时会程序会崩溃现象。希望懂的朋友能告知下~。

下面是串口数据处理任务,这里删去了其他代码,只把他打印出来了而已。

void task1_task(void *p_arg)
{
 OS_ERR err;
 OS_MSG_SIZE Usart1_Data_size;
 u8 *p;

 while(1)
 {
  p=(u8*)OSTaskQPend((OS_TICK  )0, //请求消息队列,获得储存区首地址
   (OS_OPT    )OS_OPT_PEND_BLOCKING,
   (OS_MSG_SIZE* )&Usart1_Data_size,
   (CPU_TS*   )0,
   (OS_ERR*   )&err);

  printf("%s\r\n",p);        //打印数据

  delay_ms(100);
  OSMemPut((OS_MEM* )&UART1_MemPool,    //释放储存区
  (void*   )p,
  (OS_ERR*  )&err);

  OSSemPost((OS_SEM* )&SEM_IAR_UART,    //释放信号量
  (OS_OPT  )OS_OPT_POST_NO_SCHED,
  (OS_ERR* )&err);

  OSTimeDlyHMSM(0,0,1,500,OS_OPT_TIME_PERIODIC,&err);     
 }
}

来源:STM32嵌入式开发
免责声明:本文为转载文章,转载此文目的在于传递更多信息,版权归原作者所有。本文所用视频、图片、文字如涉及作品版权问题,请联系小编进行处理(联系邮箱:cathy@eetrend.com)。

围观 541

页面

订阅 RSS - 串口