一、STM32里的串口通信
在STM32里,串口通信是USART,STM32可以通过串口和其他设备进行传输并行数据,是全双工,异步时钟控制,设备之间是点对点的传输。对应的STM32引脚分别是RX和TX端。STM32的串口资源有USART1、USART2、USART3.
串口的几个重要的参数:
- 波特率,串口通信的速率
- 空闲,一般为高电平
- 起始位,标志一个数据帧的开始,固定为低电平。当数据开始发送时,产生一个下降沿。(空闲–>起始位)
- 数据位,发送数据帧,1为高电平,0为低电平。低位先行。
比如 发送数据帧0x0F 在数据帧里就是低位线性 即 1111 0000 - 校验位,用于数据验证,根据数据位的计算得来。有奇校验,偶校验和无校验。
- 停止位,用于数据的间隔,固定为高电平。数据帧发送完成后,产生一个上升沿。(数据传输–>停止位)
下方就是一个字节数据的传输过程,从图中可以看出,串口发送的数据一般都是以数据帧的形式进行传输,每个数据帧都由起始位,数据位,停止位组成, 且停止位可变。
二、串口的发送和接收
USART是STM32内部集成的硬件外设,可以根据数据寄存器的一个字节数据自动生成数据帧时序,从TX引脚发送出去,也可以自动接收RX引脚的数据帧时序,拼接成一个字节数据,存放在数据寄存器里。
当配置好USART的电路之后,直接读取数据寄存器,就可以自动发送数据和接收数据了。在发送和接收的模块有4个重要的寄存器
- 发送数据寄存器TDR
- 发送移位寄存器,把一个字节的数据一位一位的移出去
- 接收数据寄存器RDR
- 接收移位寄存器,把一个字节的数据一位一位的接收
下方为串口的发送和接收图解:
串口发送
在配置串口的各个参数时,可以选择发送数据帧的数据位的大小,可选8位或9位。
串口发送数据实际上就是对发送数据寄存器TDR进行写操作。
1. 当串口发送数据时,会检测发送移位寄存器是不是有数据正在移位,如果没有移位,那么这个数据就会立刻转移到发送移位寄存器里。准备发送。
2. 当数据移动到移位寄存器时,会产生一个TXE发送寄存器空标志位,该位描述如下。当TXE被置1,那么就可以在TDR写入下一个数据了。即发送下一个数据。
3. 发送移位寄存器在发送器控制的控制下,向右移位,一位一位的把数据传输到TX引脚。
4. 数据移位完成后,新的数据就会再次从TDR转移到发送移位寄存器里来,依次重复1-3的过程。通过读取TXE标志位来判断是否发送下一个数据。
串口接收
- 数据从RX引脚通向接收移位寄存器,在接收控制的控制下,一位一位的读取RX的电平,把第一位放在最高位,然后右移,移位八次之后就可以接收一个字节了。
- 当一个字节数据移位完成之后,这一个字节的数据就会整体的移到接收数据寄存器RDR里来。
- 在转移时会置RXNE接收标志位,即RDR寄存器非空,下方为该位的描述。当被置1后,就说明数据可以被读出。
下图即为串口接收的工作流程
三、串口在STM32中的配置
首先要明确几点:使用STM32串口外设中的哪一个?串口发送或者接收数据?串口相关的参数配置?发送或接收是否使用到中断?
下方为串口发送的配置。
1. RCC开启USART、串口TX/RX所对应的GPIO口
RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2,ENABLE); //开启USART2的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE); //开启GPIOA的时钟
12
2. 初始化GPIO口
这里注意哈,根据自己的需求来配置GPIO口,发送和接收是都需要还是只需要其中一个。然后对应的根据引脚定义表来初始化对应的GPIO口。
USART2对应的引脚
USART1对应的引脚
这里根据手册来看,RX引脚模式配置成浮空输入或者上拉输入。TX引脚模式配置成复用推挽输出。
**比如我这里只初始化TX发送端**
1
//TX端
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_AF_PP; //复用推挽输出
GPIO_InitStructure.GPIO_Pin=GPIO_Pin_2; //USART2对应的TX端为GPIOA2
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz; //50MhZ
GPIO_Init(GPIOA,&GPIO_InitStructure);
123456
3. 串口初始化
注意哈,USART_Init()这个函数,是用来配置串口的相关参数的。
- USART_BaudRate 串口通信使用的波特率 一般是9600或者是115200,这里我们给9600
- USART_HardwareFlowControl 是否选择硬件流触发,一般这个我们也不选,所以选择无硬件流触发。
- USART_Mode 这个参数要注意了哈,串口的模式,发送模式还是接收模式,还是两者都有
- USART_Parity 校验位,可以选择奇偶校验和不校验。没有需求就直接无校验
- USART_StopBits 停止位 有1、0.5、2位,我们这里选1位停止位
- USART_WordLength 数据位 有8位和9位可以选择
//串口初始化
USART_InitTypeDef USART_InitStruct;
USART_StructInit(&USART_InitStruct); //初始默认值
USART_InitStruct.USART_BaudRate=9600;
USART_InitStruct.USART_HardwareFlowControl=USART_HardwareFlowControl_None; //不使用硬件流触发
USART_InitStruct.USART_Mode=USART_Mode_Tx; //TX 发送模式
USART_InitStruct.USART_Parity=USART_Parity_No; //不选择校验
USART_InitStruct.USART_StopBits=USART_StopBits_1; //停止位1位
USART_InitStruct.USART_WordLength=USART_WordLength_8b; //数据位8位
USART_Init(USART2,&USART_InitStruct);
12345678910
4. 串口使能
//串口使能
USART_Cmd(USART2,ENABLE);
12
5. 串口发送数据
注意哈,我们要判断TXE标志位的状态。0,数据还没有被转移到移位寄存器;1,数据已经被转移到移位寄存器。当TXE标志位为1时,就说明可以发送下一个数据了。详细过程可看上面串口发送的解释。
串行通信的通信方式
同步通信:带时钟同步信号传输 SPI,IIC通信接口
异步通信:不带时钟同步信号。-UART(全双工通用异步收发器),单总线
同步通信才需要打开硬件数据流控
void uart_init(u32 bound)
{
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
//①串口时钟使能,GPIO 时钟使能,复用时钟使能
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1|
RCC_APB2Periph_GPIOA, ENABLE); //使能 USART1,GPIOA 时钟
//②串口复位
USART_DeInit(USART1); //复位串口 1 (也可以不用复位)
//③GPIO 端口模式设置
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; //ISART1_TX PA.9
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽输出
GPIO_Init(GPIOA, &GPIO_InitStructure); //初始化 GPIOA.9
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10; //USART1_RX PA.10
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; //浮空输入
GPIO_Init(GPIOA, &GPIO_InitStructure); //初始化 GPIOA.10
//④串口参数初始化
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); //初始化串口
#if EN_USART1_RX //如果使能了接收
//⑤初始化 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); //中断优先级初始化
//⑤开启中断
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); //开启中断
#endif
//⑥使能串口
USART_Cmd(USART1, ENABLE); //使能串口
}
接受协议格式
//接收状态
//bit15, 接收完成标志
//bit14, 接收到0x0d
//bit13~0, 接收到的有效字节数目
u16 USART_RX_STA=0; //接收状态标记
void USART1_IRQHandler(void) //串口1中断服务程序
{
u8 Res;
if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) //接收中断(接收到的数据必须是0x0d 0x0a结尾)
{
Res =USART_ReceiveData(USART1); //读取接收到的数据
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;//接收数据错误,重新开始接收
}
}
}
}
}
void Send_data(u8 *s)
{
while(*s!='\0')
{
while((USART1->SR&0X40)==0);//循环发送,直到发送完毕
// USART_SendData(USART1,*s);
USART1->DR = (u8) *s;
s++;
}
}
基本通信协议小例
[0x2c,0x12,aver[0],width,hight,int(length),0x5b]//下位机传来数据格式
u8 USART_RX_BUF[USART_REC_LEN]; //接受缓冲区
u8 count=0;
void Receive_Data(u8 data)
{
static u8 state =0;
if((state==0)&&(data==0x2c))
{
state=1;
USART_RX_BUF[count++]=data;
}else if((state==1)&&(data==0x12))
{
state=2;
USART_RX_BUF[count++]=data;
}else if(state==2)
{
USART_RX_BUF[count++]=data;
if(count>10||data==0x5b)
state=3;
}else if(state==3)//开始检测数据是否正常
{
if(USART_RX_BUF[count-1]==0x5b)//数据正常
{
state=0;
USART_ITConfig(USART1,USART_IT_RXNE,DISABLE);//关闭DTSABLE中断
x_error =USART_RX_BUF[2];
width =USART_RX_BUF[3];
hight =USART_RX_BUF[4];
y_error =USART_RX_BUF[5];
length =USART_RX_BUF[6];
USART_RX_STA = 1;
Count = 0;
USART_ITConfig(USART1,USART_IT_RXNE,ENABLE);//开启ENABLE中断
}else //数据不正常错误
{
state = 0;
count=0;
}
}
else//没接收到state=3
{
state = 0;
count=0;
}
}
环形缓冲区
#include "drv_opt.h"
/*--------------------------------------------------------------------------------*/
static loop_t do_loop;
static loop_pt ploop;
/*----------------------------------------------------------------------*/
//从环形缓冲区中读出数据
static int loop_read(loopbuf_pt ploop,void *vbuf,unsigned char len)
{
int i = 0;
char *buf = (char *)vbuf;
for(i=0; i<len; i++)
{
if(ploop->rear == ploop->head) //为空
{
break;
}
buf[i] = ploop->buffer[ploop->rear];
ploop->rear ++;
if(ploop->rear >= Loop_MAXBUFLEN)
ploop->rear = 0;
}
return i;
}
/*----------------------------------------------------------------------*/
static void loop_write(loopbuf_pt ploop,u8 dat)
{
//printf("%c \r\n",dat);
if( (ploop->head+1) % Loop_MAXBUFLEN == ploop->rear )//满了
{
/*TODO: Add error handling here.*/
//debug_Assert(0,DPFL,"UART0:buffer full.");
}
else
{
ploop->buffer[ploop->head] = dat;
ploop->head++;
if(ploop->head >= Loop_MAXBUFLEN)
ploop->head = 0;
}
}
/*----------------------------------------------------------------------*/
static loopbuf_pt loop_create(void)
{
loopbuf_pt tloop = (loopbuf_pt) mem_alloc( sizeof(loopbuf_t) );
tloop->lock = thread_cslock_init("new loop");
return tloop;
}
/*----------------------------------------------------------------------*/
loop_pt loop;
void init_drv_loop_port(void)
{
ploop = &do_loop;
ploop->create = loop_create;
ploop->read = loop_read;
ploop->write = loop_write;
loop = ploop;
}
typedef struct{
thread_cslock_t lock;
int head;
int rear;
u8 buffer[Loop_MAXBUFLEN];
}loopbuf_t,*loopbuf_pt;
typedef struct{
int (*read)(loopbuf_pt ploop,void *vbuf,unsigned char len);
void (*write)(loopbuf_pt ploop,u8 dat);
loopbuf_pt (*create)(void);
}loop_t,*loop_pt;
gpio模拟串口
void send_byte(uint8_t data){
Set_TX(0); //开始位
delay_us(104);//波特率 9600
for(int i = 0; i < 8; i++){
if(data & 0x01){
Set_TX(1);
}
else{
Set_TX(0);
}
delay_us(104);
data = data >> 1;
}
Set_TX(1);//停止位
delay_us(104);
常见的接收数据搭配
1.串口接收中断+串口空闲中断
- 在接收中断中将数据保存到数组
- 在空闲中断中进行接收数据包校验
- 接收完一个字节会发生一次接收中断,接受完一帧数据会进入一次空闲中断。
2.DMA传输+串口空闲中断
-
每一次接收标志被置位时,USART_DR数据将自动传送到指定地址。
-
当接收完成DMA控制器指定的传输量时,DMA控制器在该DMA通道的中断矢量上产生一中断(使能DMA中断的情况下产生中断)
-
在空闲中断中进行接收数据包校验
KEIL中支持printf
#include<stdio.h>
//加入以下代码,支持printf函数,而不需要选择use MicroLIB
#if 1
#pragma import(__use_no_semihosting)
//标准库需要的支持函数
struct __FILE
{
int handle;
};
FILE __stdout;
//定义_sys_exit()以避免使用半主机模式
void _sys_exit(int x)
{
x = x;
}
//重定义fputc函数
int fputc(int ch, FILE *f)
{
while((USART1->SR&0X40)==0);//循环发送,直到发送完毕
USART1->DR = (u8) ch;
return ch;
}
#endif
/*使用microLib的方法*/
/*
int fputc(int ch, FILE *f)
{
USART_SendData(USART1, (uint8_t) ch);
while (USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET) {}
return ch;
}
int GetKey (void) {
while (!(USART1->SR & USART_FLAG_RXNE));
return ((int)(USART1->DR & 0x1FF));
}