STM32 串口

USART 通信协议

  所谓协议,就是一种交换信息的规定。想要传输信息,必须按照一定的规则才能识别。USART是串行,全双工,异步通信

RS-232与TTL电平

TTL: 0~3.3/5V 一般芯片输出的都是TTL;
RS-232:-15~15V,但-15V对应逻辑1,+15V对应逻辑0。主要用于工业设备的通信,高的电平差有较高的容错能力。

USB转串口 :电平转换芯片有CH340,PL2303,CP2102;需要安装驱动。指南者上的是CH340,通过跳帽连接USART1;

串口通信数据包组成

起始位:一个逻辑0表示
结束位:一般由一个1个逻辑1表示,也可选择其他方式。
有效数据位:数据位后面就是数据位,stm32中为9个位。
校验位:可以使用校验来提高稳定性。

  1. 偶校验:添加校验位后,校验位和有效数据位的1有偶数个。
  2. 奇校验:添加校验位后,校验位和有效数据位的1有奇数个。

USART功能框图讲解

引脚

TX:数据发送。
RX:数据接收。
SCLK:时钟,仅同步通信使用(一般使用异步)
nRTS: 请求发送
nCTS: 允许发送
这些引脚外设可以在数据手册 3 Pinouts and pin descriptions 查到,下面是VET6芯片的引脚总结。
在这里插入图片描述这里列出的是串口复用功能的默认端口,在重映射下,这些功能可以对应其他端口,这样同样可以在数据手册这一章查到。在参考手册 8.3 有更集中的总结。

注意这里只有USART1是挂在在APB2总线下的,其于串口在APB1总线下,打开时钟的时候需要注意。

寄存器

状态寄存器(USART_SR)

  其反应了发送过程中,发送状态的改变

数据寄存器(USART_DR)

  9位有效,其包含一个发送数据寄存器TDR和接收数据寄存器RDR。

控制寄存器(USART_CR)

  一共有3个控制寄存器,这三个寄存器完成对USART通信模式的控制,如使能,字长,校验等。并且其中有对发送和接收中断的配置。
  发送时,起始的配置位有 CR1UE,TE,RE 位。

波特比率寄存器(USART_BRR)

在这里插入图片描述

配置波特率,这个配置需要配置整数和小数部分。这样是因为配置波特率的公式中需要用到小数。
  stm32波特率计算公式是:
b a u d = f C K 16 ∗ U S A R T D I V baud = \frac{f_{CK}}{16*USARTDIV} baud=16USARTDIVfCK
b a u d baud baud是波特率, f C K f_{CK} fCK是时钟频率, U S A R T D I V USARTDIV USARTDIV就是要配置寄存器写入值
如:想要波特率为115200,配置时钟为72M,可以算出,USARTDIV=39.0625。
  如何配置呢?对于整数部分,直接写入39,16进制为0x17
  对于小数,寄存器中留有四个二进制位,可以表示16个数,也就是1被16等分,0001就表示 1 16 \frac{1}{16} 161,因此想要得到0.625,就应该写入0.625/ 1 16 \frac{1}{16} 161=0x01。如果为0.1875,0.1875/ 1 16 \frac{1}{16} 161=0x03
  最终,向寄存器写入0x171表示39.0625.若写入0x173则表示39.1875。(注意这里表示为16进制数)

发送过程中寄存器的变化

  1. 发送数据
    从CPU或DMA读取数据到TDR,放到发送移位寄存器,一位一位从TX发送出去。
    SR寄存器的 TXE 会置1,表示数据已经移动到移位寄存器。
    发送完成 TC 置1。
  2. 接收数据
    数据从 RX传来,接收移位寄存器接收,传给RDR,传给CPU。
    SR寄存器的 RXNE 置1,表示接收到数据可以读取。

固件库编程

结构体

其实真正的寻找应该从函数开始,到结构体。但那样太乱,这先介绍结构体。

初始化结构体 USART_InitTypeDef

typedef struct
{
  uint32_t USART_BaudRate;             //配置波特率

  uint16_t USART_WordLength;           //字长   
  uint16_t USART_StopBits;            //终止位个数

  uint16_t USART_Parity;              //校验位
  uint16_t USART_Mode;               //模式:发送,接收

  uint16_t USART_HardwareFlowControl;  //硬件控制流
} USART_InitTypeDef;

固件库函数

这里列出一些重要的函数。

  • USART_Init:初始化串口,其传入了对应串口和初始化结构体,因此配置了结构体中的内容。对应了CR,BRR寄存器的置位。
  • USART_Cmd:使能对应串口,对应CR寄存器中的置位操作。
  • USART_ITConfig:中断使能,对应CR寄存器中的置位操作。
  • USART_SendData,USART_ReceiveData:发送,接收;对应于DR寄存器的操作。
  • USART_GetITStatus:获取标志位,对应SR寄存器的操作。

开始编程(一)

两个编程任务,第一实现串口发送;第二,实现接收返回,和串口控制led灯。第一部分完成串口的发送。

初始化

  使用USART接收电脑发送的数据,并且返回这个数据。使用中断完成

GPIO

  因为USART是复用功能,从GPIO原理框图可以看出,必须初始化GPIO。PA9为发送,初始化为推挽输出;PA10为输入,初始化为浮空输入;打开时钟;

static void USART_GPIO_Config(void)
{
	GPIO_InitTypeDef GPIO_InitStruct;
	
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
	
	GPIO_InitStruct.GPIO_Pin   = GPIO_Pin_9;
	GPIO_InitStruct.GPIO_Mode  = GPIO_Mode_AF_PP;
	GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA,&GPIO_InitStruct);
	
	GPIO_InitStruct.GPIO_Pin   = GPIO_Pin_10;
	GPIO_InitStruct.GPIO_Mode  = GPIO_Mode_IN_FLOATING;
	GPIO_Init(GPIOA,&GPIO_InitStruct);
}

USART

初始化USART,配置波特率115200,字长8,停止位1,无校验位,无硬件流。
这里注意
USART_InitStruct.USART_Mode = USART_Mode_Tx | USART_Mode_Rx;
这个骚操作,直接把输入和输出模式一起配置了。
注意,最后要使能 USART

static void USART_Init_Config(void)
{
	USART_InitTypeDef USART_InitStruct;
	
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);
	
	USART_InitStruct.USART_BaudRate   = 115200;
	USART_InitStruct.USART_WordLength = USART_WordLength_8b;
	USART_InitStruct.USART_StopBits   = USART_StopBits_1;
	USART_InitStruct.USART_Parity     = USART_Parity_No;
	USART_InitStruct.USART_Mode       = USART_Mode_Tx | USART_Mode_Rx;
	USART_InitStruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
	
	USART_Init(USART1,&USART_InitStruct);
	USART_Cmd(USART1,ENABLE);
}

中断配置

中端的配置为了第二个实验的接收

配置外设中断

USART的中断配置十分简单,一个函数就可以解决。

	USART_ITConfig(USART1,USART_IT_RXNE,ENABLE);

对串口1配置,接收中断。

配置中断优先级分组

配置分组为1

NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1);

配置 NVIC 寄存器

这里要注意 NVIC_IRQChannel 这个结构体成员变量可赋值的查找。详细请见STM32 EXTI外部中断小结 配置 NVIC 寄存器 部分。

static void USART_NVIC_Config(void)
{
	NVIC_InitTypeDef NVIC_InitStruct;
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1);//配置优先级分组
	
	NVIC_InitStruct.NVIC_IRQChannel = USART1_IRQn;
	NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
	NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 1;
	NVIC_InitStruct.NVIC_IRQChannelSubPriority = 1;
	
	NVIC_Init(&NVIC_InitStruct);
}

  下面应该写中断配置函数,在中断配置函数中读取输入数据,然后发送回电脑。这部分内容放在实验二,现在先把上面的代码整合一下。

整合配置代码

  这样所有的初始化工作完成,编写一个函数集成上面的函数。

void USART_Config(void)
{
	USART_GPIO_Config();    //配置GPIO端口
	USART_Init_Config();    //配置USART模式
	USART_ITConfig(USART1,USART_IT_RXNE,ENABLE);  //使能中断,接收中断
	USART_NVIC_Config();    //配置中断管理
}

配置到这里,程序已经可以使得芯片接收数据,并且跳转到中断中了。

发送数据

  为了完成在中断中读取数据和发送数据我们必须编写函数。
  在USART库中有函数 USART_SendData,这个函数实际上已经可以发送一些数据了,这里对其进行包装。使用函数检测是否发送完成。其实这里不用检测。直接使用USART_SendData也可完成这项工作。

void USART_SentByte(USART_TypeDef* pUSARTx, uint8_t data)
{
	USART_SendData(pUSARTx, data);
	while( USART_GetFlagStatus(pUSARTx,USART_FLAG_TXE) == RESET);
}

  函数需要传入USART外设的指针和要发送的数据。
  另外注意。虽然USART_SendData 的第二个参数可以传入16位数据,但实际上只有低8位有效,也就是说,使用一次USART_SendData 只能发送一个字节的数据。
  注意:单片机发出的信号其实是一串二进制码,串口调试助手默认是以ASCII码的方式显示。在keil中,发送的数据也以ASCII码的形式编码。

  这是一张ASCII码对应表,具体可见ASCII码对照表
  如:USART_SentByte(USART1, 0x41),这里单片机把 0x41 编码为2进制 0100 0001 ,通过串口发送出去,串口调试助手默认把这串二进制码用ASCII解释,显示A
  如: USART_SentByte(USART1,'A') ,这里keil把 A 按ASCII码编码为0100 0001,通过串口发送出去,串口调试助手认把这串二进制码用ASCII解释,显示A。但如果选中16进制显示,则会显示FF41
  因为只能发送一个字节,我们在编写一个可以发送两个字节。

void USART_Sent2Byte(USART_TypeDef* pUSARTx, uint16_t data)
{
	uint8_t temp_h,temp_l;
	temp_h = (data >> 8) & 0x00ff;
	temp_l = data&0x00ff;
	
	USART_SendData(pUSARTx, temp_h);
	while( USART_GetFlagStatus(pUSARTx,USART_FLAG_TXE) == RESET);
	
	USART_SendData(pUSARTx, temp_h);
	while( USART_GetFlagStatus(pUSARTx,USART_FLAG_TXE) == RESET);
}

重新定向printf

其实实际使用中用的最多的是使用重新定向后的printf。记得要在头文件中引用C库 #include <stdio.h>

//重新定向C库函数,以便于使用printf
int fputc(int ch, FILE *f)
{
		USART_SendData(USART1, (uint8_t) ch);
		while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);		
		return (ch);
}

//重新定向C库函数,以便于使用scanf
int fgetc(FILE *f)
{
		while (USART_GetFlagStatus(USART1, USART_FLAG_RXNE) == RESET);
		return (int)USART_ReceiveData(USART1);
}

更具体的这些函数做到了什么,我也没学,没法解释。但实验的结果可以看出,在printf() 的括号中填入任何长度的字符,都可打印出来。

用宏定义封装

为了使得代码有较好的移植性,我们使用宏定义对代码进行封装。

  1. GPIO部分
    GPIO肯定是要对引脚和GPIOx进行封装。对时钟函数中的参数封装。这里使用和野火教程中一样的封装。
// GPIO
#define  DEBUG_USART_GPIO_CLK           (RCC_APB2Periph_GPIOA)
#define  DEBUG_USART_TX_GPIO_PORT       GPIOA   
#define  DEBUG_USART_TX_GPIO_PIN        GPIO_Pin_9
#define  DEBUG_USART_RX_GPIO_PORT       GPIOA
#define  DEBUG_USART_RX_GPIO_PIN        GPIO_Pin_10
  1. USART部分
    要封装USARTx,因不同的USART会对应不同的总线,因此不仅要对时钟函数的参数封装,还要封装时钟函数;另外为了以后便于调节波特率,把波特率也用宏定义封装。
//USART
#define  DEBUG_USARTx                   USART1
#define  DEBUG_USART_CLK                RCC_APB2Periph_USART1
#define  DEBUG_USART_APBxClkCmd         RCC_APB2PeriphClockCmd
#define  DEBUG_USART_BAUDRATE           115200

3.中断部分
初始化NVIC中,channel的选择和中断服务函数的编写用到了USARTx

#define  DEBUG_USART_IRQ                USART1_IRQn
#define  DEBUG_USART_IRQHandler         USART1_IRQHandler

这样,整个程序就有了对于USARTx之间的移植性,只要修改宏定义中的参数,就可以开启不同的USART。

开始编程(二)

第二部分完成数据的接收返回和控制LED。

编写中断服务函数

上面已经完成中断配置的前三个步骤了,这里完成最后一个上面已经完成中断配置的前三个步骤了,这里完成最后一个。
中断服务函数最好写在stm32f10x_it.c中,其名称必须为startup_stm32f10x_hd.s 中定义的名称。

void DEBUG_USART_IRQHandler(void)
{
	uint8_t Sentback;
	//进入中断后再次检测是否真的置位
	if( USART_GetFlagStatus(DEBUG_USARTx,USART_IT_RXNE) != RESET)
	{
		Sentback = USART_ReceiveData(DEBUG_USARTx);
		USART_SendData(DEBUG_USARTx,Sentback);//使用这个函数把得到的数据返回
		//也可用printf("%d",Sentback);代替
	}
}

控制LED

直接修改中断服务函数,使用 getchar() 获得输入;使用 switch构成判断。

void DEBUG_USART_IRQHandler(void)
{
	uint8_t LED_Common;
	LED_Common = getchar();
	
	switch(LED_Common)
	{
		case '1':LED_R(ON);printf("LED_R ON!\n");Infor_LED[0]=1;break;
		case '2':LED_G(ON);printf("LED_R ON!\n");Infor_LED[1]=1;break;
		case '3':LED_B(ON);printf("LED_R ON!\n");Infor_LED[2]=1;break;
		
		case '4':LED_R(OFF);printf("LED_R OFF!\n");Infor_LED[0]=0;break;
		case '5':LED_G(OFF);printf("LED_R OFF!\n");Infor_LED[1]=0;break;
		case '6':LED_B(OFF);printf("LED_R OFF!\n");Infor_LED[2]=0;break;
	}

发送和接收过程中的编码

  上面提到,如果使用USART_SentByte(USART1, A),函数会把发送的数据以ASIIC码编码,通过串口发送,A 的 ASIIC码 为0100 0001。串口调试助手默认以 ASIIC码 解码 显示 A。若选择 以16进制显示 则显示 41

  同样对于电脑端发送的数据,串口调试助手也以 ASIIC码 编码发送。如发送 A,会编码 0100 0001 发送。若选择以16进制发送,则会发送 1010

sizeof

  有了串口程序,stm32和电脑就可以通信了,这样我们就可以展开一些实验,如这里介绍的sizeof。
  sizeof可以返回对应值的长度,在不同的机器上返回值不同。在stm32上写下如下代码。

#include "stm32f10x.h"
#include "bsp_usart.h"
#include <stdio.h>

int text(int a[])
{
	uint8_t i =sizeof(a);
	return i;
}

int main(void)
{	
	int a[5] = {1,2,3,4,5};
	uint8_t b    = sizeof(int);
	USART_Config();
  
  	printf("b=%d\n",b);
	printf("sizeof a = %d\n",sizeof(a));
  	printf("fun:sizeof a = %d",text(a));
  	while(1)
	{	
		
	}	
}

返回值为:

b=4
sizeof a = 20
fun:sizeof a = 4

  这里我们调用 sizeof 查看 int 的数据宽度为4(b对应的值)。在main中调用 sizeof 查看数组 a 的宽度为20,查看函数 text 宽度为4.
  这和普通PC返回的值不一样。在64位电脑上,使用VScode运行上面这段代码,获得的结果如下:

b=4
sizeof a = 20
fun:sizeof a = 8

  先解释 b=4;这一点stm32和电脑返回值一致,因为int类型占用的就是4个数据位。
  sizeof a,查看main函数中a数组的宽度。返回20,表示4×5=20(数据宽度×数据个数)。
  fun:sizeof a,查看函数返回的a的数据宽度。这里PC返回8,stm32返回4。和上面直接在main函数中查看不同。这是因为,向函数传输数组时,传输的不是数组本身,而是数组的首地址(指针)。在32位机中,指针长度为4,64位机中,指针长度为8.
  这也提示我们,在函数中,我们无法使用sizeof得到传入数组的大小。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值