一、Usart
1.帧格式
如果你做过单片机实时接收传感器的数字信号进行处理相关的项目,肯定对帧格式收发熟悉。一帧数据是人为的定义帧头、帧尾、数据长度、校验位等,通过单片机的Usart进行发送和接收,其中可以利用串口空闲中断函数来接收帧格式数据,串口空闲中断也就是接收一帧数据后产生中断。这时其实我有一个疑问的:
在发送一帧数据中,每个字节之间有时间间隔吗?时间间隔是多少呢?
直到我看了这一篇解释:
Modbus通讯(这个协议自行百度)时规定主机发送完一组命令(自定义帧)必须间隔3.5个字符再发送下一组新命令,这个3.5字符主要用来告诉其他设备这次命令(数据)已结束,而这个3.5字符的时间间隔采用以下方式计算:1个字符包括1位起始位、8位数据位(一般情况)、1位校验位(或者没有)、1位停止位(一般情况下)。这样说起来一般情况下1个字符就包括10位,那么3.5个字符就是3.5*10=35位
这就引出来全双工串行通信接口USART的帧格式(不是人为定义的),包括8个数据bit,1个停止bit,(起始1bit是必须的)默认无奇偶,无流控。基本上也就是10Bit组成串口的一帧数据,帧间隔也就是35位的时间。
注意:以上的帧间隔和帧格式是usart默认的,而我们自己需要的数据也有帧间隔和帧格式,例如:
计算机 ←← 设备: 0xF0 0x1F 0x06 0x32 JYL JYH MBL MBH CHECK
采样率为200HZ,也就是5ms发送一次数据帧。
下图可以直观看出:
2.波特率
波特率含义是每秒传输的二进制位的个数,每秒传输多少比特。单位是bit/s或者是波特 :这里说一句是在信号被调制以后在单位时间内的变化,所以波特率通信的数据格式一般是8+1+1的格式,也就是实际发送了一个字节。
这里以我的项目所用的帧格式和波特率为例:256000bps+帧长度:9字节
意思就是说每1秒(也就是1000毫秒)传输256000个位,
反过来说传输256000个二进制位需要1000毫秒,传输一位需要1/256ms
那么一帧数据也就是需要90/256=0.36ms
帧间隔是:35/256=0.14ms
MODBUS RTU要求一帧数据起始和结束至少有大于等于3.5个字符的时间
在波特率为256000的情况下,只要大于0.14毫秒即可!
一般,为了简单起见,可以将传输45Bit的时间四舍五入后的整型值作为两个数据帧之间的时间间隔,并以此来判断报文接收的完整性。
这里提一点:Usart通信是异步通信,和同步通信的区别就是不需要使用时钟控制,但是波特率发生器是用一个定时器来实现的。
3.串口usart通信
串口的功能是十分强大的,不仅可以作为数据传输,还可以用作程序的调试输出功能。下面以STM32F103Usart1为例子说明:
1.首先进行串口的配置工作:
void uart_init(u32 bound){
//GPIO端口设置
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1|RCC_APB2Periph_GPIOA, ENABLE); //使能USART1,GPIOA时钟
//USART1_TX GPIOA.9
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; //PA.9
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; //输出的最大速率50MHZ
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽输出,端口的复用,IO复用为串口
GPIO_Init(GPIOA, &GPIO_InitStructure);//初始化GPIOA.9
//USART1_RX GPIOA.10初始化
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;//PA10
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;//浮空输入
GPIO_Init(GPIOA, &GPIO_InitStructure);//初始化GPIOA.10
//Usart1 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); //根据指定的参数初始化VIC寄存器
//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); //初始化串口1
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);//开启串口接受中断
USART_Cmd(USART1, ENABLE); //使能串口1
}
推挽复用输出
推挽电路是由两个三极管或MOSFET,以推挽方式存在于电路中,电路工作时,两只对称的开关管每次只有一个导通,所以导通损耗小、效率高、既提高电路的负载能力,又提高开关速度。
当内部输出1电平时,上边的MOS管导通同时下边的MOS管截至,IO口输出高电平。
当内部输出0电平时,上边的MOS管截至同时下边的MOS管导通,IO口输出低电平。
推挽方式可完全独立产生高低电平 ,推挽方式为低阻,这样,才能保证口线上不分走电压或分走极小的电压(可忽略),保证输出与电源相同的高电平。推挽适用于输出而不适用于输入,因为若对推挽(低阻)加高电平后,I=U/R,I会很大,将造成口的烧毁。适用于大功率输出。
浮空输入
浮空输入状态下,IO的电平状态是不确定的,完全由外部输入决定,如果在该引脚悬空的情况下,读取该端口的电平是不确定的。
上拉输入:上拉电阻的目的是为了保证在无信号输入时输入端的电平为高电平。而在信号输入为低电平是输入端的电平应该也为低电平。
下拉输入:下拉电阻的目的是为了保证在无信号输入时输入端的电平为低电平。
具体可以参考:blog.csdn.net/hbaizj/arti…
2.两个重要的寄存器USART_DR USART_SR
USART_DR是一个双寄存器,既可以发送也可以接受。当向该寄存器写数据时串口会自动发送,当接受数据时也会保存在该寄存器中。常用的两个函数:
void USART_SendData(USART_TypeDef* USARTx, uint16_t Data) //数据发送
{
assert_param(IS_USART_ALL_PERIPH(USARTx)); //内存对齐
assert_param(IS_USART_DATA(Data));
USARTx->DR = (Data & (uint16_t)0x01FF);
}
//数据接收
int16_t USART_ReceiveData(USART_TypeDef* USARTx)
{
assert_param(IS_USART_ALL_PERIPH(USARTx));
return (uint16_t)(USARTx->DR & (uint16_t)0x01FF);
}
USART_SR是串口状态寄存器,其中相关的中断、查询操作都要用到该寄存器。其中RXNE位被置1时代表已经接收数据并且可以读出来,然后清除该位(可以自动清零也可以手动清零);TC位置位代表已经发送成功,也可以产生相应的发送中断,然后清除该位(可以自动清零也可以手动清零)。常用的函数:
//读取串口状态
FlagStatus USART_GetFlagStatus(USART_TypeDef* USARTx,uint16_t USART_FLAG)
//配置中断
void USART_ITConfig(USART_TypeDef* USARTx, uint16_t USART_IT, FunctionalState NewState)
//中断状态查询
ITStatus USART_GetITStatus(USART_TypeDef* USARTx, uint16_t USART_IT)
//中断状态常用的就是接受中断和发送中断
3.中断执行
当外界条件触发中断时,CPU先进行保存现场,然后去执行中断函数。中断函数一般为:
void USART1_IRQHandler(void) { } //功能按照自己的需要来
4.printf函数的应用
printf("num = %d\r\n",num) 主要应用在调试数据,通过上位机来观察数据
参考原子的代码,在STM32中使用printf()
//加入以下代码,支持printf函数,而不需要选择use MicroLIB
#pragma import(__use_no_semihosting)
//标准库需要的支持函数
struct __FILE
{
int handle;
};
FILE __stdout;
//定义_sys_exit()以避免使用半主机模式
_sys_exit(int x)
{
x = x;
}
//重定义fputc函数
int fputc(int ch, FILE *f)
{
while((USART1->SR&0X40)==0);//循环发送,直到发送完毕
USART1->DR = (u8) ch;
return ch;
}
4.使用DMA
ST大叔:CPU看你好累,给你个免费的机器人玩玩!
CPU:大叔啊,我没时间给你说话啊,串口那个烦人的家伙又催我收数据了。
ST大叔:这个机器人叫DMA,DMA(直接存储器访问)是一种数据传输方法,利用DMA控制器,可以把数据直接从一个地址空间复制到另一个地址空间。以后把数据收发的活交给他干就行了。
咱们以串口通信为例说明,利用DMA最常用的两种方式:
- 串口寄存器数据直接存放到ROM/FLASH开辟的缓存空间中
- 在ROM/FLASH开辟的缓存空间直接把数据发送到串口寄存器中
CPU心想这下好多了,串口老弟不用每次收到数据都找我处理了,可以先把数据交给我的DMA机器人,然后我需要的时候再把它提取出来就行了。
CPU:嗯嗯,这个机器人不错,那怎么使用啊?
ST大叔:嗯嗯,我就仔细给你介绍一下把!我考虑到DMA的单身问题,我就设计了两个DMA,男女搭配,干活不累嘛!男的叫DMA1,女的叫DMA2,这DMA1具有7个数据通道,DMA2具有5个数据通道,不同的通道连接不同的外设。比如说UART串口,UART1_TX使用通道4,UART1_RX使用通道5,如果你想传输数据,分别开启相应的通道开关就行。为了他们的输出速率,我使用了双AHB总线架构,一个访问存储器,一个访问IO。下面看看具体的配置:
//DMA_CHx:DMA通道CHx
//cpar:外设地址
//cmar:存储器地址
//cndtr:数据传输量
void MYDMA_Config(DMA_Channel_TypeDef* DMA_CHx,u32 cpar,u32 cmar,u16 cndtr)
{
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); //使能DMA传输
DMA_DeInit(DMA_CHx); //将DMA的通道1寄存器重设为缺省值
DMA1_MEM_LEN=cndtr;
DMA_InitStructure.DMA_PeripheralBaseAddr = cpar; //DMA外设ADC基地址
DMA_InitStructure.DMA_MemoryBaseAddr = cmar; //DMA内存基地址
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST; //数据传输方向,从内存读取发送到外设
DMA_InitStructure.DMA_BufferSize = cndtr; //DMA通道的DMA缓存的大小
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_Medium; //DMA通道 x拥有中优先级
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; //DMA通道x没有设置为内存到内存传输
DMA_Init(DMA_CHx, &DMA_InitStructure); //根据DMA_InitStruct中指定的参数初始化DMA的通道USART1_Tx_DMA_Channel所标识的寄存器
}
//开启一次DMA传输
void MYDMA_Enable(DMA_Channel_TypeDef*DMA_CHx)
{
DMA_Cmd(DMA_CHx, DISABLE ); //关闭USART1 TX DMA1 所指示的通道
DMA_SetCurrDataCounter(DMA1_Channel4,DMA1_MEM_LEN);//DMA通道的DMA缓存的大小
DMA_Cmd(DMA_CHx, ENABLE); //使能USART1 TX DMA1 所指示的通道
}
CPU:嗯嗯,挺简单的,但我又一个问题啊,如果我开启了两个通道,有一个通道的数据非常紧急,那该怎么办呢?
ST大叔:没好好看代码吧,我考虑到这个情况了,我设计了DMA优先级,你可以在配置的时候设置一下通道的优先级,如果你忘记设置了,那么默认的通道1的优先级最高,通道7优先级最低。
CPU:嗯嗯了解。那么我怎么知道DMA发送完或者接收完数据了呢,我不想去看着它。
ST大叔:中断啊老弟,我设计了DMA可以触发中断,你只要开启DMA中断就行,如果开启中断提醒之后,他会提醒你:主人,您交待的一次任务完成了,下面要干啥啊?然后你把命令写到DMA中断服务函数里面就行了。提醒你一句,所有的中断服务函数都是要先清除中断标志的哦,否则会死机哦!
CPU:好的,大叔想的真全面啊,那我问最后一个问题啊,如果我要通过DMA从外设不断接受一定长度的数据进行解析,提取里面的关键信息,具体该怎么办呢?
ST大叔:这个好办啊,你忘记了你有一个串口空闲服务函数嘛?你可以开启它啊,就是接受完一帧数据之后,进入到串口空闲服务函数,然后进行数据的解析等工作,对了你要串口空闲中断服务函数在先关闭一下DMA,以避免有别的数据进行干扰,还有就是依次读一下USART->SR,USART->DR,这是清除空闲中断的方法,一般人我不告诉他哦。
CPU:知道了,有没有demo我运行一下啊,试试效果。
ST大叔:自己写(或者私信我)。不给你说了,我还要去参加牛客的笔试呢。。。。。。。。。。。。。。。。。。。。。(下一篇牛客笔试的输入输出问题)
5.总结
这做项目中,经常使用串口数据的收发问题,UART+DMA是一个不错的选择。在过程需要注意的问题简单的写了一下。
参考博客: