串口

嵌入式开发中,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)。

围观 20

基于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)。

围观 256

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)。

围观 294

今天再给大家分享一些关于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)。

围观 3141

串口发送数据

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)。

围观 108

一、STM32用USART发送字符串

void UART_Send_Message(u8 *Data)
{
  while(*Data!='\0')
  {
      USART_SendData(USART1, *Data);
      while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);//读取串口状态
      Data++;
  }
}
 
void main(void)
{
  u8 str_buf[500];
  memset((char *) &str_buf, 0, sizeof(str_buf));
  UART_Send_Message(str_buf);
}

“串口发送的这几种写法,你用过几种?

“串口发送的这几种写法,你用过几种?
while(SET == USART_GetFlagStatus(USART1,USART_FLAG_RXNE));

含义是:当接收引脚有数据时,状态寄存器的USART_FLAG_RXNE就会为1,此时USART_GetFlagStatus(USART1,USART_FLAG_RXNE)的返回值就为1(SET),若无数据则为RESET。

二、USART_FLAG_TXE和USART_FLAG_TC怎么用

这里主要说的是在特殊情况下发送字符软件代码的写法。

特殊情况指的是:

1)调用发送字符串函数“发送完”本机立即掉电;

2)调用发送字符串函数“发送完”从机立即掉电;

【上面两种主要用于芯片对电源控制的项目中】

3)调用发送字符串函数“发送完”立刻进入待机或停机;

其实本文主要说的是两个标志位:USART_FLAG_TXE 和 USART_FLAG_TC.

USART_FLAG_TXE发送缓冲区空标志:说明可以往数据寄存器写入数据了,但并不代码数据发送完成了。

USART_FLAG_TC发送完成标志:这个才是代表USART在缓冲区的数据发送完成了,即从机接收到了数据。

这两个标志的区别在于:它们分别表示数据在发送过程中,在两个不同的阶段中的完成情况。TXE 表示数据被从发送缓冲区中取走,转移到的移位寄存器中,此时发送缓冲是空的,可以向其中补充新的数据了。而 TC 则表示最后放入发送缓冲区的数据已经完成了从移位寄存器向发送信号线 Tx 上的转移。所以,判定数据最终发送完成的标志是 TC,而不是 TXE。

下面讲述在不同代码写法下,得到不同实验效果【调试助手接收数据】:

常见写法一

“串口发送的这几种写法,你用过几种?"

“串口发送的这几种写法,你用过几种?"

这种写法在不是特殊(不掉电、不待机等)情况下,问题不大,USART数据会成功发送出去。但是在上面说的特殊情况下,问题就来了,代码只将数据放到了发送缓冲区,而没有发送出去就掉电或待机了,这个时候其实最后两个字符是没有发送出去的。

2、常见写法二

“串口发送的这几种写法,你用过几种?"

“串口发送的这几种写法,你用过几种?"

这种写法达到的效果和上面存在不同的就是倒数第二个数据发送出去了,也就是只有最后一个字符是没有发送出去的。

3、常见写法三

“串口发送的这几种写法,你用过几种?"

“串口发送的这几种写法,你用过几种?"

这种写法达到的效果和上面两种写法有不一样,发送了10个字符。

4、写法四

“串口发送的这几种写法,你用过几种?"

“串口发送的这几种写法,你用过几种?"

这种写法按理说可以实现功能,但实际多次试验结果确实第一字节数据丢失了。

5、写法五(正确写法)

“串口发送的这几种写法,你用过几种?"

“串口发送的这几种写法,你用过几种?"

这种写法是比较完成,为了保守起见,在特殊情况下使用该写法。。

至此,常见软件写法都给出了源代码和试验效果,请根据自己实际情况写代码验证。

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

围观 94

1、前言

玩过Linux的朋友, 是不是对Linux无所不能的串口Shell命令控制台羡慕不已, 要是自己做的STM32F系列低档次的MCU也有这种控制交互能力, 会给调试/维护和配置省下多少麻烦事呀, 比如启动/关闭调试或自检模式, 打印调试信息, 配置系统参数, 传输文件等等, 也有相当多的朋友凭借自己出色的编程能力可以实现这些功能, 这里提出我的这个解决方案, 以作交流.

本平台(xc_shell)具备以下性能特点:

1、大量主要代码, 和具体硬件无关, 移植性强,代码文件少.

2、只有在处理用户的输入命令时, 才占用CPU资源, 且代码可裁剪到1KB SRAM和4KB Flash;

3、用户可以非常灵活的添加按模板编写的命令脚本文件, 自定义扩张能力强.

4、支持操作系统和非操作系统两种场景应用.

5、支持Ymodem文件传输协议

6、支持将Flash的扇区开辟为参数区, 可实现本地/远程升级。

7、实用Led灯信号管理, 可将65535虚拟信号灯选择输出到1个实体LED灯上, 调试时序和状态非常有用

8、拥有基础的LED管理, 调试模式设置, 命令帮助指令, 复位指令等基础功能

功能越多设计会越复杂, 为了解释清楚代码, 先向大家解释一下以上功能的基础实现原理, 并提供一个最小的的源码工程。

2、xc_shell平台介绍

2.1 如何实现硬件无关

类比Linux会发现, 设备的硬件接口往往会被虚拟成一个文件(驱动), 而Linux内核完全与硬件系统无任何字节关联, 不同平台驱动不同而已, 故而本xc_shell的串口驱动也采用了相似的思路:

1) 串口驱动用一个结构体描述, 这样只需在xc_shell.c中用指针指向这个TTYx_HANDLE结构体对象就可以将串口(tty)硬件与内核联系在一起, 聪明的朋友可能会想到, 假如我将带网络的开发板按此结构体,虚拟一个TTY对象, 岂不是就可以实现一个网络远程控制台了! 这点确实是可以的!

2) 当然诸如多TTY串口实现接口互换等, 都是一个指针和step2中的注入回调处理交换的问题。

3)用户在使用api_TxdFrame或api_TxdByte时”bsp_ttyX.c“,会驱动具体MCU的串口将数据发送出去, 收到一帧数据后,若用户设置了inj_RcvFrame回调处理方法,则会在中断中执行用户的回调处理。

/*---------------------* 
*     指正函数定义
*----------------------*/
typedef void    (*pvFunDummy)(void);
 
//输入整行,输出逻辑
typedef void    (*pvFunVoid) (void);
typedef void    (*pvFunBool) (bool     bVal);
typedef void    (*pvFunChar) (uint8_t  cVal);
typedef void    (*pvFunShort)(uint16_t sVal);
typedef void    (*pvFunWord) (uint32_t wVal);
 
//输入整行,输出逻辑
typedef bool    (*pbFunVoid) (void);
typedef bool    (*pbFunBool) (bool     bVal);
typedef bool    (*pbFunChar) (uint8_t  cVal);
typedef bool    (*pbFunShort)(uint16_t sVal);
typedef bool    (*pbFunWord) (uint32_t wVal);
 
//输入整形指针,输出逻辑
typedef bool    (*pbFun_pVoid) (void * pVoid);
typedef bool    (*pbFun_pChar) (uint8_t  * pStr);
typedef bool    (*pbFun_pShort)(uint16_t * pShor);
typedef bool    (*pbFun_pWord) (uint32_t * pWord);
 
//输入数据帧,输出逻辑
typedef bool    (*pbFun_Buffx)(void * pcBuff, uint16_t len );
typedef bool    (*pbFun_Bytex)(uint8_t * pcByte, uint16_t len );
/*---------------------* 
*    TTYx 句柄结构
*----------------------*/
typedef struct TTYx_HANDLE_STRUCT 
{
    const char  * const name;       //驱动器名
    const uint16_t      rxSize;     //接收大小
    const uint16_t      txSize;     //发送大小
    
    //------------------------------------------------------
    //step1: 用户可用API
    const pvFunWord     init;           //初始化.
    const pbFun_Bytex   api_TxdFrame;   //发送数据帧. (发送帧)
    const pbFunChar     api_TxdByte;    //发送数据字节
    
    //------------------------------------------------------
    //step2: 注入回调函数
    pbFun_Bytex         inj_RcvFrame;   //(ISR)接收数据帧. (接收帧)
    pvFunDummy          inj_TxdReady;   //(ISR)发送完毕回调
    
    //------------------------------------------------------
    //step3: 接收回调函数
    struct TTYx_HANDLE_STRUCT * pvNext; //连接到下一个指令 
}TTYx_HANDLE;

可注入的命令脚本(CLI)实现
命令CLI也是一个结构体对象:

/*---------------------* 
*       CLI指令
*----------------------*/
typedef struct
{
 const char * const  pcCmdStr;     //指令字符串(只能为小写字母)
 const char * const  pcHelpStr;     //指令描述,必须以:"\r\n结束". 比如:"help: Returns a list\r\n".
 const pFunHook      pxCmdHook;     //指向回调函数的指针,处理成功返回真否者返回0;
 uint8_t             ucExpParam;     //指令期望的参数个数
 const MEDIA_HANDLE *phStorage;      //指向存储介质,没有的话填充NULL  
}Cmd_Typedef_t;

各位朋友可能会使用到非常多的自定义CLI命令, 格式诸如这个网卡的命令:

const Cmd_Typedef_t CLI_WizMsg=
{
    //识别关键字
    .pcCmdStr   = "wiz",
    //帮助内容
    .pcHelpStr  =
    "[WIZ contorls]\r\n"
 " wiz help\r\n"
 " wiz rd info\r\n"
 " wiz reset\r\n"
 " wiz wr ip <D0>.<D1>.<D2>.<D3>\r\n"
 " wiz wr mask <D0>.<D1>.<D2>.<D3>\r\n"
 " wiz wr way <D0>.<D1>.<D2>.<D3>\r\n"
 " wiz wr mac <H0>-<H1>-<H2>-<H3>-<H4>-<H5>\r\n"
 " wiz wr port <udp> <bak> <vol> <pic>\r\n"
 " wiz wr sip <D0>.<D1>.<D2>.<D3> <port>\r\n"
 " wiz wr cip <D0>.<D1>.<D2>.<D3> <port>\r\n"
 " wiz load default\r\n"
 "[WIZ Test mode]\r\n"
 " wiz loop open\r\n"
 " wiz loop close\r\n"
 "\r\n",
 
 //处理函数
 .pxCmdHook  = &Shell_WIZ_Service,        //见实体函数
 
 //附带数据
 .ucExpParam = 0,
 
  #ifdef SHELL_USE_YMODEM  
    //存储介质
    .phStorage  = NULL,
  #endif
};
/*---------------------* 
*       CLI 链表节
*----------------------*/
Cmd_List_t  WizList   = { &CLI_WizMsg,   NULL,}; //Shell指令的头

如配置IP地址输入“wiz wr ip 192.168.1.250\r\n”则可以了

3、环境准备

3.1 硬件开发环境

  • STM32F103系列开发板一块, 带USART1接口。
  • USB转串口线一根。

3.2 软件开发环境

  • MDK4.72或以上
  • SecureCRT串口超级终端

3.3 软件配置

在xc_shell使用“/r/n”作为命令的结束符, 而SecureCRT按下Enter不是输入“/r/n”故而需要按下图设置:此外在《终端》/仿真/高级中选取【本地回显】

“实用STM32的串口控制平台的实现(建议收藏)"

4、代码介绍

4.1 目录结构

□ XC_SHELL
├──┬── BSP_LIB    BSP库,硬件相关驱动代买
│  ├──── bsp_ledx.c   基础LED驱动      
│  └──── bsp_tty0.c   调试串口驱动
│
├──┬──MDK-ARM     工程文件
│  └──── Project.uvproj
│
├──┬──SHELL_CFG   SHELL配置头文件
│  └──── user_eval.h
│
├──┬──SHELL_CORE  SHELL内核文件
│  ├──── xc_shell.c   SHELL内核文件
│  ├──── xc_ymodem.c  Ymodem传输协议(默认关闭,在xc_shell.h中启动)
│  ├──── xc_iap.c     Flash的IAP操作,需要bsp_flash.c驱动支持
│  └──── shell_iap.c  shell的用户脚本模板
│
├──┬──SHELL_INC   SHELL头文件
│  ├──── bsp_type.h   驱动结构定义
│  ├──── xc_shell.h   SHELL头文件
│  └──── xconfig.h    硬件无关配置文件
│
├──┬──STM32F10x_StdPeriph_Lib_V3.5.0  STM32的标准外设库
│  └──── ......
│
└──┬──USER        用户文件    
   ├─ .....       
   └──── main.c        main文件

4.2 工程设置要点

1) 设置使用微库:

“实用STM32的串口控制平台的实现(建议收藏)"

2)配置包含和路径,注意使用了“--c99”标准,如图

“实用STM32的串口控制平台的实现(建议收藏)"

3) 编译工程,无错误警告后烧写程序到开发板运行。

4.3 最终效果

按图输入一下指令,SHELL平台会回复相关信息。其中输入“led set 0=1”会将信号1分配到物理LED0上;输入“led set 0=2”会将信号2分配到物理LED0上。这样用户编写程序代码时相当于拥有了超级多的LED信号可用,调试时序非常有用。

“实用STM32的串口控制平台的实现(建议收藏)"

5、添加自己的指令脚本

5.1 源代码示例

假设我要编写一个自己的指令脚本, 来读取MCU的关键信息,关键字为mcu, 文件命名为shell_mcu.c;当输入“mcu rd 0”时显示MCU的FLASH大小,输入“mcu rd 1”时读取MCU的唯一ID信息。shell_mcu.c源代码:

/*********************************Copyright (c)*********************************
**                               
**                                 FIVE工作组
**
**---------------------------------File Info------------------------------------
** File Name:               shell_mcu.c
** Last modified Date:      2017/9/17 15:13:57
** Last Version:            V1.0
** Description:             shell测试
**
**------------------------------------------------------------------------------
** Created By:              wanxuncpx
** Created date:            2017/9/17 15:14:08
** Version:                 V1.0
** Descriptions:            none
**------------------------------------------------------------------------------
** HW_CMU:                  STM32F103
** Libraries:               STM32F10x_StdPeriph_Lib_V3.5.0
** version                  V3.5
*******************************************************************************/
 
/******************************************************************************
更新说明:
******************************************************************************/
 
/******************************************************************************
*********************************  编 译 控 制 ********************************
******************************************************************************/
#define MCU_SHELL               //注释掉时屏蔽iap shell功能
 
#include "xc_shell.h"       //Shell支持文件,含bool,uint8_t..以及串口数据收发操作
/******************************************************************************
********************************* 文件引用部分 ********************************
******************************************************************************/
/*---------------------* 
*     模块驱动引用
*----------------------*/
//#include "net_w5500.h"
 
#ifdef MCU_SHELL
/******************************************************************************
********************************** Shell实例 **********************************
******************************************************************************/
/*---------------------* 
*      CLI指令服务
*----------------------*/
extern bool Shell_MCU_Service(void * pcBuff, uint16_t len );
 
/*---------------------* 
*       CLI 结构
*----------------------*/
const Cmd_Typedef_t CLI_McuMsg=
{
    //识别关键字
    "mcu",
    
    //帮助内容
 "[mcu contorls]\r\n"
 " mcu rd <d>\t\t- Read FLASH information.\r\n"
 "\r\n",
 
 //处理函数
 &Shell_MCU_Service,
 
 //附带数据
 0,
  #ifdef SHELL_USE_YMODEM  
    //存储介质
    NULL,
  #endif
};
 
/*---------------------* 
*     CLI链表节(输出)
*----------------------*/
Cmd_List_t  McuList  = {&CLI_McuMsg      ,NULL}; //IAP指令链表
 
/******************************************************************************
********************************* 函 数 声 明 *********************************
******************************************************************************/
/******************************************************************************
/ 函数功能:STM32F103控制函数
/ 修改日期:2015/7/14 20:22:02
/ 输入参数:none
/ 输出参数:none
/ 使用说明:需要执行约10s
******************************************************************************/
static bool FLASH_ioctl(uint8_t cmd,void * param)
{
    #define UID_ADDR            0x1FFFF7E0  //闪存容量寄存器,值对应KB单位
    #define MAC_ADDR            0x1FFFF7E8  //MCU的唯一ID号,共12个字节
    #define UID_SIZE            2           //UID的字节数
    #define MAC_SIZE            12          //MAC的字节数
 
    //step1: 检查参数
    if(!param)return false;
        
    //step2: 处理数据
    switch(cmd){
      case 0 : {       //获取FLASH的的UID
        uint16_t * ptDst = (uint16_t *)((uint32_t)param+1);
        
        *ptDst = *(uint16_t *)UID_ADDR;
        *(uint8_t  *)param =  UID_SIZE;
        return true;
      }
      case 1 : {       //获取芯片的MAC地址
        uint32_t * ptDst = (uint32_t *)((uint32_t)param+1);
        uint32_t * ptSrc = (uint32_t *)MAC_ADDR;
    
        *ptDst++ = *ptSrc++;
        *ptDst++ = *ptSrc++;
        *ptDst++ = *ptSrc++;
        *(uint8_t  *)param = MAC_SIZE;
        return true;
      }
      default:return false;
    }
}
 
/******************************************************************************
/ 函数功能:文件系统Shel指令处理
/ 修改日期:2013/9/10 19:04:15
/ 输入参数:输入当前的程序版本
/ 输出参数:none
/ 使用说明:none
******************************************************************************/
bool Shell_MCU_Service(void * pcBuff, uint16_t len )
{
    uint8_t    *ptRxd;          //用于接收指令处理
    int         i;
    uint16_t    retval;
    uint8_t     buff[32];
    
    //处理指令
    //--------------------------------------------------------------------------
    ptRxd = (uint8_t *)pcBuff;
    
    if(StrComp(ptRxd,"rd ")) //读取FLASH信息
    {
        int wval;
        
        if(1 != sscanf((void *)ptRxd,"%*s%d",&wval) )return false;
        if( wval>2 )return false;
        if(0==wval) {
            FLASH_ioctl(0,buff);
            retval = *(uint16_t *)(buff+1) ;
            printf("->Flash:\t%dKB\r\n",retval);
            return true;
        }
        else if(1==wval) {
            FLASH_ioctl(1,buff);
            printf("->MAC:\t ");
            for(i=0; i<buff[0]-1; i++){printf("%02X-",buff[i+1]);}
            printf("%02X\r\n",buff[i+1]);
            return true;
        }
        else return false;
    }
    else if(StrComp(ptRxd,"help\r\n"))      //指令帮助
    {
        shell_SendStr((void *)CLI_McuMsg.pcHelpStr);
        return true;
    }
    else return false;
}
 
/******************************************************************************
***********************************   END  ************************************
******************************************************************************/
#endif

5.2 实现步骤

1) 将该文件添加到工程下。

2) 在main.c中用extern 引用McuList,源代码为:

/*---------------------* 
*     Shell指令链表
*----------------------*/
extern Cmd_List_t  McuList;

3)在main.c初始化时添加:

//----------------------------------------------------------
//step1: shell初始化
shell_Init(115200,ledx_cfg);        //初始化shell接口
CLI_AddCmd(&McuList);     //添加模块指令到链表

4)编译工程文件。

5)下载到开发板运行即可在终端下看到新支持的CLI指令:

“实用STM32的串口控制平台的实现(建议收藏)"

版权声明:本文为CSDN博主「wanxuncpx」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:
https://blog.csdn.net/wanxuncpx/article/details/78009490

围观 127

页面

订阅 RSS - 串口