STM32f1库函数开发学习

实战二 · 串口通信

1. 背景知识

  •  DMA
  •  通信方式
  •  LIN总线

  •  DMA,Direct Memory Access,存储器直接访问,一种高速数据传输操作,允许外设与存储器、外设与外设之间直接交换数据。

    CPU 和 DMA 控制器的传输过程处于并行操作状态,大大提高整个计算机系统效率。

    适用于一些高速的I/O设备(kBps),例如磁盘存取、图像处理、高速数据采集、同步通信中的收/发信号。

    DMAC,DMA控制器,负责DMA传送全过程控制的硬件电路。

    DMA

    参考文章:DMA总结


  •  通信方式
    数据传输,按照数据流方向可分为三种传输方式:

    • 单工通信
    • 半双工通信
    • 全双工通信

    单工通信只支持单方向传输数据,任何时候不能改变传输方向。为了保证数据不失真,需要校验位,校验出错时通过监控信道发送请求重发信号。

    适用于数据收集系统,例如计算机和打印机,只有计算机向打印机传输数据。

    半双工通信,支持两个方向传输,但同一时刻只能存在一个方向上的传输,是一种可调换方向的单工通信,例如对讲机。

    全双工通信,允许数据同时在两个方向上传输,有两个传输道路,是两个单工通信结合。

    全双工通信效率高,控制简单,造价高,例如手机、计算机。


  •  LIN总线

    Local Interconnect Network,本地互联网络总线。

    LIN主要功能是为CAN总线网络提供辅助功能:

    • 单主多从组网方式,最多16节点,1主15从
    • 硬件要求低,只需要UART/SCI接口,几乎所有的MCU都支持LIN
    • 不需要单独的晶振
    • 只需要一根信号线,单总线设备
    • 传输速率20Kbps
    • 新节点不影响原有节点的硬件

    LIN网络节点任务分为主机任务和从机任务,从机任务在主机节点和从机节点都可以运行。

    LIN通信可以用作“主从通信”、“基于时间表的通信”,后期可以做个多机通信处理系统?

    参考文章:





2. usart文件夹介绍

usart文件夹包括 .c 文件和 .h 文件,针对串口1进行了初始化和中断接收,用其他串口时需要更改。主要包括两个函数:

  1. uart_init 函数,串口初始化
  2. USART1_IRQHandler 函数 ,中断响应函数



下面来解剖这两个函数


1.uart_init 函数

  1. 引入32位参数 波特率(bound)
void uart_init(u32 bound)

  1. 定义结构体
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
NVIC_InitTypeDef NVIC_InitStructure;

  1. 开启时钟使能
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1|RCC_APB2Periph_GPIOA
|RCC_APB2Periph_AFIO, ENABLE);
//使能 USART1,GPIOA 时钟
//以及复用功能时钟

使用一个内置外设的时候,要首先使能相应的GPIO时钟,然后使能复用功能时钟内置外设时钟

不知道内置外设应该开启哪个时钟使能的时候,在参考手册搜索“系统架构/系统结构”:
系统结构图

  1. 初始化GPIO端口为特定状态
//USART1_TX PA.9 
 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; 
 //PA.9 
 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
 //端口速度
 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; 
 //复用推挽输出模式
 GPIO_Init(GPIOA, &GPIO_InitStructure); 
 //初始化 GPIOA.9 发送端
 
 //USART1_RX PA.10 
 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
 //PA.10
 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
 //浮空输入
 GPIO_Init(GPIOA, &GPIO_InitStructure); 
 //初始化 GPIOA.10 接收端

GPIO配置步骤:

  • 时钟使能 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
  • 引脚号 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_x;
  • 端口翻转速度 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
  • 引脚模式 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
  • 端口初始化 GPIO_Init(GPIOA, &GPIO_InitStructure);

如何查找GPIO端口应该配置为什么模式


在《STM32中文参考手册》中搜索“外设的GPIO配置”,得到以下几个表格(以下不全):
GPIO配置1
GPIO配置2

GPIO配置3

  1. Usart_1 NVIC 中断优先级配置
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn; 
//对应中断通道
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=3 ;
//抢占优先级 3
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3; 
//子优先级 3
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; 
//IRQ 通道使能
NVIC_Init(&NVIC_InitStructure); 
//中断优先级配置

NVIC_IRQChannel:

定义初始化的是哪个中断,这个我们可以在 stm32f10x.h 中找到每个中断对应的名字。

例如 USART1_IRQn。

  1. 串口1初始化参数以及使能
//USART 初始化设置

USART_InitStructure.USART_BaudRate = bound;
//波特率;
USART_InitStructure.USART_WordLength = USART_WordLength_8b;
//字长为 8 位
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_RXNE, ENABLE);
 //开启中断
 USART_Cmd(USART1, ENABLE); 
 //使能串口


串口配置的一般步骤

  1. 串口时钟使能,GPIO时钟使能
  2. 串口复位
  3. GPIO端口模式设置
  4. 串口参数初始化
  5. 开启中断,初始化NVIC
  6. 使能串口
  7. 中断处理函数

几个配置需要的库函数:

1. 串口时钟使能

查询系统架构图可知,USART1挂在APB2下,需要开启时钟:

RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1);

2. 串口复位
复位用于设备异常时重新配置,系统刚开始工作时也需要复位。
USART_DeInit(USART1); //复位串口 1

3. 串口参数初始化

需要初始化的参数是:波特率、字长、停止位、奇偶校验位、硬件数据流控制、收发模式

通过初始化函数完成:
USART_Init(USART1, &USART_InitStructure);
结构体的成员变量配置示例如下

USART_InitStructure.USART_BaudRate = bound; 
//波特率
USART_InitStructure.USART_WordLength = USART_WordLength_8b;
//字长为 8 位数据格式
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); 
//初始化串口

4. 数据发送与接收

STM32的发送接收通过寄存器USART_DR实现,这是一个双寄存器,包含了TDR和RDR,写数据时串口就自动发送,收到数据时存储在内

发数据:
void USART_SendData(USART_TypeDef* USARTx, uint16_t Data);

读数据:
uint16_t USART_ReceiveData(USART_TypeDef* USARTx);

5. 串口状态

通过寄存器USART_SR读取串口状态,一共有32位只取前10位,一般我们只关注第5、6位RXNE和TC:

USART_SR
RXNE(读数据寄存器非空):该位为1的时候,说明有数据被接收到,并且可读。此时需要尽快读取USART_DR,然后将RXNE清零或者直接置0清除

TC(发送完成):该位被置位时,USART_DR数据已经发送完成,可以设置中断。也有两种清零方式:读USART_SR,写USART_DR;直接将TC写0

读取串口状态的库函数:

USART_GetFlagStatus(USART1, USART_FLAG_RXNE);

USART_GetFlagStatus(USART1, USART_FLAG_TC);

串口的状态是通过宏定义实现的:

#define USART_IT_PE ((uint16_t)0x0028)
#define USART_IT_TXE ((uint16_t)0x0727)
#define USART_IT_TC ((uint16_t)0x0626)
#define USART_IT_RXNE ((uint16_t)0x0525)
#define USART_IT_IDLE ((uint16_t)0x0424)
#define USART_IT_LBD ((uint16_t)0x0846)
#define USART_IT_CTS ((uint16_t)0x096A)
#define USART_IT_ERR ((uint16_t)0x0060)
#define USART_IT_ORE ((uint16_t)0x0360)
#define USART_IT_NE ((uint16_t)0x0260)
#define USART_IT_FE ((uint16_t)0x0160)

6. 串口使能

通过函数USART_Cmd()实现:

USART_Cmd(USART1, ENABLE); 
//使能串口

7. 开启串口响应中断

当我们需要开启串口中断的时候,需要使能,例如:

USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
//开启中断,接收到数据中断

此函数的第二个入口参数是使能串口的类型,例如此例中我们需要在接收到数据的时候产生中断,就需要开启RXNE的中断USART_IT_RXNE

如果需要在发送数据结束的时候产生中断,则:

USART_ITConfig(USART1,USART_IT_TC,ENABLE);
//数据发送结束产生串口中断

8. 获取中断状态

比如我们使能了某个串口发生中断,当中断发生了,可以调用函数判断是否完成中断:

USART_GetITStatus(USART1, USART_IT_TC)

返回值是SET,则串口发送完成中断





2. USART1_IRQHandler

USART1_IRQHandler函数是串口1的中断响应函数,串口1发生中断时会跳转到其中去执行

  1. if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)

用上面列举的常用库函数中第八句(获取中断状态),判断是否接受中断,如果接收了中断则读取串口接收到的数据:

  1. Res = USART_ReceiveData(USART1);//(USART1->DR); //读取接收到的数据

数据赋给变量Res。读到数据之后就对数据进行分析。

这里有一个接收协议,利用数组USART_RX_BUF[],接收状态“寄存器”USART_RX_STA (实为一个全局变量,但是起到类似寄存器的作用),实现对串口数据的接收管理。

  • USART_RX_BUF[] 的长度由USART_REC_LEN定义:
    u8 USART_RX_BUF[USART_REC_LEN];

    USART_REC_LEN是位于 usart.h中定义的一个全局参数,定义最大接收的字节数

  • USART_RX_STA 是一个接收状态寄存器,定义表如下:

USART_SR
当接收到数据时,把数据保存在USART_RX_BUF[]中,同时在接收状态寄存器(USART_RX_STA)中计数接收到的有效数据个数。

当接收到回车( 回车由0X0D和0X0A组成 ) 的第一个字节0X0D (0x0D,asc码是13,指的是回车\r,把光标置于本行行首) 时,停止计数;

等待0X0A (0x0A,asc码是10,指的是换行 \n,把光标置于下一行的同一列) ,标记USART_RX_STA的第15位接收完成标志,完成一次接收,等待第15位被清除后完成一次接收。

如果0X0D回车来迟,而数据超过USART_REC_LEN时,会丢弃前面的数据重新接收

配置示例

void USART1_IRQHandler(void) //串口 1 中断服务程序
{
	u8 Res;
	#if SYSTEM_SUPPORT_OS //如果 SYSTEM_SUPPORT_OS 为真,则需要支持 OS
	OSIntEnter(); 
	#endif

	if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) 
	//接收中断(接收到的数据必须是 0x0d 0x0a 结尾) 		
	{
		Res =USART_ReceiveData(USART1);//(USART1->DR); //读取接收到的数据
		if((USART_RX_STA&0x8000)==0)//接收未完成
		{
			if(USART_RX_STA&0x4000)//接收到了 0x0d
			{
				if(Res!=0x0a)USART_RX_STA=0;//接收错误,重新开始
				else USART_RX_STA|=0x8000; //接收完成了
			}
		else //还没收到 0X0D
			{
				if(Res==0x0d)USART_RX_STA|=0x4000;
				else
				{
					USART_RX_BUF[USART_RX_STA&0X3FFF]=Res ;
					USART_RX_STA++;
					if(USART_RX_STA>(USART_REC_LEN-1))USART_RX_STA=0;
					//接收数据错误,重新开始接收 
				} 
			}
		} 
	} 
	
#if SYSTEM_SUPPORT_OS //如果 SYSTEM_SUPPORT_OS 为真,则需要支持 OS
OSIntExit(); 
#endif
}

关于OS操作系统的研究日后再进行






3. 硬件电路

查看原理图:
U1
USART1的RXD和TXD位于PA10和PA9,再次查找得到电气连接方式:

电气连接

这里发现串口1的TXD和RXD需要用跳线帽跟PA9、PA10连接在一起






4. 主函数的一些说明

usart.h文件中可以引入"stdio.h"头文件,并且加入一段代码,即可提供printf()函数支持,直接利用printf函数向串口发送我们需要的内容。原理及操作见下文:

STM32使用printf打印串口

我们来看一下主函数设计的几个示例及要点:

1. 接收数据部分

if(USART_RX_STA & 0x8000)
{ 
	len = USART_RX_STA&0x3fff;//得到此次接收到的数据长度
	printf("\r\n 您发送的消息为:\r\n");
	for(t=0;t<len;t++){
		USART1->DR = USART_RX_BUF[t];
		while((USART1->SR & 0X40) == 0);}//等待发送结束
	printf("\r\n\r\n");//插入换行
	USART_RX_STA = 0;
}

USART_RX_STA的bit15表示接收完成标志,bit14表示接收到0X0D


USART_RX_STA&0x8000,即bit15位比较,若为1则接受完成,之后再接收判断长度。


判断长度就是剩余14位比较,USART_RX_STA&0x3fff,0x3fff即0011 1111 1111 1111,bit相同则为1否则为0,便可得到USART_RX_STA的低14位的值,便得到其长度

2. 发送数据部分

else{
		times ++;
		if(times % 5000 == 0){
			printf("\r\n123456789\r\n");
			printf("asdfghjkl\r\n\r\n\r\n");}
		if(times % 200 == 0)printf("hello world\r\n");  
		if(times % 30 == 0)LED0 =! LED0;
		//LED闪烁指示系统还在运行
		delay_ms(10);   
	}