在设计实现某种功能的系统时,常需要不同设备之间的通信。通信主要有串行和并行两种方式,应用比较广泛的UART(Universial Asynchonous Receiver Transmitter)通用异步收发器,SPI(Serial Peripheral Interface)串行外设接口,I2C(Inter Integrated Circuit)集成电路总线都属于这两种方式之一。下面就以应用比较广泛的串口通信作以介绍,文末附有自定义通信协议的代码。
数据按位依次传输,使用少数几条通信线路就可以完成系统间交换信息,适合于主机与主机,主机与外设之间的远距离通信。优点是占用引脚资源少,缺点是速度相比并行要慢。
按通信方向分为单工、半双工、全双工三种。
a、单工:数据传输只支持数据在一个方向上传输。
b、半双工:允许数据在两个方向上传输。但是,在某一时刻,只允许数据在一个方向上传输,它实际上是一种切换方向的单工通信;它不需要独立的接收端和发送端,两者可以合并一起使用一个端口。
c、全双工:允许数据同时在两个方向上传输。因此,全双工通信是两个单工通信方式的结合,需要独立的接收端和发送端。
按通信方式分为同步串行和异步串行。
同步串行:同步通信时,通信双方共用一个时钟,这是同步通信区分于异步通信的最显著的特点。同步通信中,数据开始传送前用同步字符来指示(常约定1~2 个),并由时钟来实现发送端和接收端的同步,即检测到规定的同步字符后,下面就连续按顺序传送数据,直到一块数据传送完毕。同步传送时,字符之间没有间隙,也不要起始位和停止位,仅在数据开始时用同步字符SYNC来指示。例如SPI,I2C通信接口。
异步串行:无时钟信号的驱动,收发双方约定数据帧的格式。每一帧由起始位、数据位、奇偶校验位和停止位组成。传送过程字符之间可以有空隙的存在。例如UART,单总线。
RXD:数据输入引脚,数据接收。
TXD:数据发送引脚,数据发送。
当ARM与电脑USB口连接传输数据时,需要一个USB转串口的装置,因为电脑无法识别TTL电平,所以需要连接一个RS232转换器将TTL电平转换成232电平。常采用的是MAX232或国产的CH340芯片。
TTL电平 | 232电平 |
---|---|
高电平 1:2.0V ~ 5V | 高电平1: -15V ~ -13V |
低电平 0:0V ~ 0.8V | 低电平0:+15V ~ +13V |
对于LVTTL,高低电平范围分别是 2.4–5V,0-- 0.4V,即LOW VOLTAGE TTL,是低电平标准的TTL。
更多RS232电平连接方式及知识可参考:链接1
详细图片可查看STM32F4数据手册26.3节。文末下载链接里有提供STM32F4中文数据手册。
数据各位同时传送,快速设备之间采用并行通信,例如CPU 与存储设备、存储器与存储器、主机与打印机等都采用并行通信。并行通信,有多少位数据就必须有多少根数据线。优点是传输速度快,效率高,多用在对实时性要求高的场合。缺点是长距离通信时抗干扰能力差,占用引脚资源多。并行通信应用较少,在此不多做介绍。
说明:本实例适合于具体项目中需要自定义帧头、帧尾等数据传输格式的应用场合。比较基础的串口设置步骤例如使能时钟,初始化参数等在此不多做介绍,具体可参考原子哥的STM32F4 开发指南(库函数版) ->5.3.3节和第九章《串口通信实验》。
更多串口接收数据的方式还可以参考:链接2
由于涉及具体项目,故不会透露太多细节,只对通信协议进行介绍,方便大家移植。进入正题。
系统分为三个模块,ARM(STM32F4)、FPGA、模拟电路。具体的系统功能由模拟电路实现。ARM负责接收来自上位机的指令,并控制FPGA输出命令信息和通信信息。
上位机与ARM通过板载的蓝牙通信,采用了HC-05蓝牙转串口模块,模块的引脚RXD与TXD分别与ARM的串口收发端相连接,这样即可实现上位机与ARM的通信。
ARM收到正确的上位机指令后,将指令或数据按照通信协议发送给FPGA,由FPGA控制模拟电路完成系统功能。
连接方式如下图所示,在图示的基础上,还要将HC-05模块的AT指令设置脚连接到ARM的IO上,AT指令用来设置蓝牙模块的参数,如设备名,密码,波特率之类。尤其要注意的是要设置蓝牙的波特率与程序中设置的串口波特率一样。设置方法见文末下载链接里的文档—HC-05芯片手册。
说明:如果是自定义帧头的话,尽量选择55,AA之类不容易在数据部分出现的码,或者是用两个字节充当帧头。
校验和为开始标识+命令号+数据长度+数据(16进制求和)。表中省略号的部分……当然是懒得算啦,一个一个加也太傻了。分享一个计算校验和的网站:链接3
当然帧头/尾、数据长度、数据、校验和这些你可以任意安排,只要上位机按照这种数据格式发送数据,作为处理器端按这种数据格式接收数据就行。这就是双方的通信协议。
数据长度与校验和都是高位在前,低位在后发送。例如命令号为01,没有数据的上位机指令为:C1D20100000194E3F4
当上位机指令有错误时,ARM会返回对应的错误号码,上位机发送指令正确,则会返回上位机发送的数据(只是帧格式中的数据,不包含命令号等其他的)。发送上位机指令用的是串口助手,串口助手设置见下图,串口助手的参数一定要跟ARM程序中设置的一样,就像两个人能正常交流那就得用约定好的语言规则。
不同指令运行结果见下图:
开发板型号:正点原子F4探索者
使用芯片:STM32F40ZGT6
开发环境:IAR
//串口初始化
void uart_init(u32 bound){
//GPIO端口设置
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA,ENABLE); //使能GPIOA时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);//使能USART1时钟
//串口1对应引脚复用映射
GPIO_PinAFConfig(GPIOA,GPIO_PinSource9,GPIO_AF_USART1); //GPIOA9复用为USART1
GPIO_PinAFConfig(GPIOA,GPIO_PinSource10,GPIO_AF_USART1); //GPIOA10复用为USART1
//USART1端口配置
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9 | GPIO_Pin_10; //GPIOA9与GPIOA10
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;//复用功能
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; //速度50MHz
GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; //推挽复用输出
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP; //上拉
GPIO_Init(GPIOA,&GPIO_InitStructure); //初始化PA9,PA10
//USART1 初始化设置
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_Cmd(USART1, ENABLE); //使能串口1
USART_ClearFlag(USART1, USART_FLAG_TC);
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);//开启相关中断
//Usart1 NVIC 配置
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;//串口1中断通道
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=3;//抢占优先级3
NVIC_InitStructure.NVIC_IRQChannelSubPriority =3; //子优先级3
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ通道使能
NVIC_Init(&NVIC_InitStructure); //根据指定的参数初始化VIC寄存器、
}
下面是串口一的中断服务函数,前面设置了字长为8位,串口的收发也是以一个字节为基本单位进行,设计思路是每接收一帧数据,会根据数据下标N来判断数据是否正确,正确则继续接收,错误则返回错误编号且初始化变量。全部接收正确则进入下一个状态。
//串口1中断服务程序
void USART1_IRQHandler(void)
{
unsigned int rec_data;
if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) //接收中断 RXNE是准备好读取接收到的数据标志位
{
USART_ClearITPendingBit(USART1, USART_IT_RXNE);//清除接受中断标志
if(N==0)
my_chk=0;//初始化和校验
rec_data = USART_ReceiveData(USART1);//赋值临时变量
my_chk+=rec_data;//计算和校验
//检验开始标识
if(N==0)
{
if(rec_data!=0xC1)
{
commu_err = PROT_W;
}
}
else if(N==1)
{
if(rec_data!=0XD2)
{
commu_err = PROT_W;
}
}
//检验命令标识
else if(N==2)
{
if( rec_data!=0x01 && rec_data!=0x02 && rec_data!=0x03
&& rec_data!=0x04 && rec_data!=0x05 && rec_data!=0x06 )
{
commu_err = CODE_W;
}
else command=rec_data;
}
//接收数据长度 2Byte
else if(N==3){
rec_length=rec_data;
}
else if(N==4)
{
rec_length = (rec_length << 8) + (rec_data); //合并数据长度
if(rec_length>599)
{
commu_err = PROT_W;
}
}
else
{
//接收数据
if(N<=rec_length+4)
{
//此处可根据不同命令号的数据长度不同,设置不同的N值筛选条件
USART_RX_BUF[N-5]=rec_data;
}
//接收和校验 2Byte
else if(N==rec_length+5)
{
rec_chk=rec_data;
}
else if(N==rec_length+6)
{
my_chk-=rec_chk;//减去接收到的校验和低位
my_chk-=rec_data;//减去接收到的校验和高位
rec_chk = (rec_chk << 8) + rec_data; //合并校验和
if(rec_chk != my_chk) //判断接收到的校验和与计算出来的校验和是否相等
{
commu_err = SUMM_W;
}
}
//接收结束标识
else if(N==rec_length+7)
{
if(rec_data!=0xE3)
{
commu_err = PROT_W;
}
}
else if(N==rec_length+8)
{
if(rec_data!=0xF4)
{
commu_err = PROT_W;
}
else
{
P2A_Flag = 1; //上位机发送数据符合协议,接收完毕进入下一个状态
}
}
}
N++;
if( commu_err == PROT_W || commu_err == CODE_W || commu_err == SUMM_W )
{
while(USART_GetFlagStatus(USART1,USART_FLAG_TXE) == RESET); //测试用,正式版本删除
USART_SendData(USART1,commu_err);
Init_USART_RX_BUF();
Init_BUF();
}
}
}
*主函数中判断接收是否完成,完成后可以进入下一个状态。*
```main.c
int main(void)
{
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//设置系统中断优先级分组2
delay_init(168); //延时初始化
uart_init(38400); //串口初始化,波特率为38400
Init_USART_RX_BUF(); //初始化接受寄存器(上位机发送到ARM的数据存储在此数组)
Init_BUF(); //过程中的变量初始化
while(1)
{
if(P2A_Flag == 1 )
{
for(j=0;j
文末下载链接内的文档如下图所示:
编号06的文档是本博客的word版,包含除了代码外的所有文字部分。
链接4