位(bit):
二进制数中的一个数位,可以是0或者1,是计算机中数据的最小单位。
字节(Byte):
计算机中数据的基本单位,每8位组成一个字节。各种信息在计算机中存储、处理至少需要一个字节。
例如,一个ASCII码用一个字节表示,一个汉字用两个字节表示。
字(Word):
两个字节称为一个字。汉字的存储单位都是一个字。
1个字节(byte)=8位(bit)数据
1个汉字是一个字,即两个字节,16位数据
通信方式-框图-参考链接
并行通信
8位数据并列的传输,传一个8位的数据是需要8根线一起传输。
串行通信
8位数据一位一位的传输,只需要一根线即可进行传输。
串行通信与并行通信特征
全双工
有两根数据线,一个用来接收数据,一个用来发送数据,互不干扰,可以同时发送和接收数据。
例如:usart(可以半双工或全双工通信),SPI(可以半双工或者全双工通信)
半双工
有两根数据线,但是不可以同时发送数据,可以分时收发数据
单工
只有一根数据线,只可以单向通信(只可以往某一个方向进行)
例如:IIC
000-串口通讯的单工、半双工和全双工的定义、区别及应用-
同步通信
数据同步方式,两个设备的时钟信号是同一个(有时钟信号的是同步)。
在传输数据时为了保证数据传输的准确性:
(1)时钟在高电平时,数据有效
(2)时钟信号在低电平时数据时无效的
(3)对时钟的要求很高(如果时钟有尖峰或者杂波,则数据传输就不准确了)
异步通信
没有时钟信号:为了保证数据传输的准确性是通过加上一些辅助的标识符
同步通信和异步通信比较
(1)在同步通信中,数据信号所传输的内容绝大部分就是有效数据。
(2)在异步通信中,传输的数据会包含有帧的各种标识符。
(3)所以同步通信的效率更好,但是对于时钟允许误差较小,异步通信对于时钟允许误差较大。
比特率:每秒钟传输的二进制数单位:bit/s
IIC,SPI(同步通信,一个时钟下传输一个数据,通过时钟来控制)
波特率:每秒钟传输的码元个数(串口)
一个二进制位表示一个码元(特殊情况下)
物理层规定我们用嘴巴还是肢体进行交流
协议层就是规定我们用汉语还是英文来交流
1、RS232和TTL就是在电平上的区别
TTL电平是直接从单片机(或者芯片)里面出来的:高电平用3.3V或者5v来表示,低电平用0表示
RS232中1用-15V表示,0用+15V表示,逻辑正好时相反的,低电平和高电平的差距非常大
TXE-发送数据寄存器空
当TDR寄存器中的数据被硬件转移到移位寄存器的时候,TXE位被硬件置位。
如果USART_CR1寄存器中的TXEIE为1,则产生中断。对USART_DR的写操作,将该位清零。
0-数据还没有转移到移位寄存器
1-数据已经转移到移位寄存器
TC-发送完成
当包含有数据的一帧发送完成后,并且TXE=1时,由硬件将TC位置’1’。
如果USART_CR1中的TCIE=1,则产生中断。由软件序列清除该位(先读USART_SR,然后写入USART_DR)。 TC位也可以通过写入’0’来清除,只有在多缓存通讯中才推荐这种清除程序。
0-发送还没有完成
1-发送已经完成
RXNE-读数据寄存器非空
当RDR移位寄存器中的数据被转移到USART_DR寄存器中,该位(RXNE)被硬件置位。
如果USART_CR1寄存器中的RXNEIE=1,则产生中断。对USART_DR的读操作可以将该位清零。 RXNE位也可以通过写入0来清除,只有在多缓存通讯中才推荐这种清除程序。
0-数据没有收到
1-收到数据可以读出
此时UE=1,TE=1
上图对应的顺序依次是1->2->3,数据来源于CPU或者DMA,数据来了之后首先是放到发送寄存器(TDR)中,然后会再放到发送数据移位寄存器中,由于数据是8位的,会将数据一位一位的发送出去(使用TX引脚)。
**
**
此时UE=1,RX=1
数据从RX引脚进来来,数据是一位一位进行接收的。
串口初始化函数
void USART_Init(USART_TypeDef* USARTx, USART_InitTypeDef* USART_InitStruct);引脚、波特率、位数,校验、时钟等
串口使能
USART_Cmd(USART1, ENABLE); //使能串口—配置的是UE位
中断使能
void USART_ITConfig(USART_TypeDef* USARTx, uint16_t USART_IT,FunctionalState NewState)
发送数据
STM32 库函数操作 USART_DR 寄存器发送数据的函数是:通过该函数向串口寄存器 USART_DR 写入一个数据。
void USART_SendData(USART_TypeDef* USARTx, uint16_t Data);
接收数据
STM32 库函数操作 USART_DR 寄存器读取串口接收到的数据的函数是:通过该函数可以读取串口接收到的数据。
uint16_t USART_ReceiveData(USART_TypeDef* USARTx);
获取标志位
该函数只判断标志位。在没有使能相应的中断函数时,通常使用该函数来判断标志位是否置1
Flagstatus USART_GetFlagStatus(USARTx,USART_FLAG)
中断状态位获取函数
不仅会判断标志位是否置1,同时还会判断是否使能了相应的中断。所以在串口中断函数中,如果要获取中断标志位,通常使用该函数。
ITStatus USART_GetITStatus(USART_TypeDef* USARTx, uint16_t USART_IT)
清楚中断标志位
Void USART_Flag_Clear(USARTx,USART_FLAG)
几个标志位函数的区分说明–链接
串口初始化函数
//初始化IO 串口1
//bound:波特率
void uart_init(u32 bound){
//GPIO端口设置
GPIO_InitTypeDef GPIO_InitStructure;//GPIO结构体指针
USART_InitTypeDef USART_InitStructure;//串口结构体指针
NVIC_InitTypeDef NVIC_InitStructure;//中断分组结构体指针
//1、使能串口时钟,串口引脚时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1|RCC_APB2Periph_GPIOA, ENABLE); //使能USART1,GPIOA时钟
//2、复位串口
USART_DeInit(USART1); //复位串口1
//3、发送接收引脚的设置
//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); //初始化PA9
//USART1_RX PA.10(有图可知浮空输入)
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;//浮空输入
GPIO_Init(GPIOA, &GPIO_InitStructure); //初始化PA10
//4、USART 初始化设置
USART_InitStructure.USART_BaudRate = bound;//一般设置为9600;
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 //如果使能了接收
//5、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寄存器
//6、开启接收数据中断
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);//开启中断
#endif
//7、使能串口
USART_Cmd(USART1, ENABLE); //使能串口
}
void Usart_SendByte(USART_TypeDef* pUSARTx,uint8_t data)
{
//调用固件库函数
USART_SendData(pUSARTx,data);//往串口中写入数据
//发送完数据是检测TXE这个位是否置1,发送数据寄存器空了,表明已经把数据传递到数据移位寄存器了
//检测TXE这个位也需要一个固件库函数
while(USART_GetFlagStatus(pUSARTx,USART_FLAG_TXE)==RESET);
//如果这个位一直为0的话就一直等待,只有当被设为SET后才会跳出这个循环(表示一个字节发送出去了)
}
在main主函数中编写语句:
//在主函数里面发送一个数据试试
Usart_SendByte(USART1,100);//往串口1中写入数据100
串口调试助手并没有显示100,而是显示的一个字母d
串口调试助手不管接收到的是什么数据,都会转化为字符
只有发送十六进制数据,串口助手使用十六进制形式接收数据时才不是字符
Usart_SendByte(USART1,'A');//往串口1里面写入一个字符A
//串口接收到字符A
串口助手无论是收发都是以字符的形式进行传输的
假如说串口助手发送一个数字1,stm32串口如果能够接收的话,在进行数据解析过程中需要按照字符 ’1‘ 来进行解析(把1当成是字符,而不是十进制1 )
有时候传感器数据可能是16位的,怎么发送?发送两个字节?
发送两个字节的数据就是十六位的。
//发送两个字节数据函数
void Usart_SendHalfWord(USART_TypeDef* pUSARTx,uint16_t data)
{
//发送十六位数据要分为两次来发送,先定义两个变量
uint8_t temp_h,temp_l;//定义8位的变量(分别存储高8位和低8位)
//首先取出高8位
temp_h=(data&0xff00)>>8;//低八位先与0相&,低8位变为0再右移8位(0xff00共16位二进制)
//再取出低8位
temp_l=data&0xff;//取出低8位数据
//16位的数据这样子就放到了两个变量里面(共16位)
//调用固件库函数
USART_SendData(pUSARTx,temp_h);//先发送高8位
while(USART_GetFlagStatus(pUSARTx,USART_FLAG_TXE)==RESET);//等待数据发送完毕
USART_SendData(pUSARTx,temp_l);//再发送低8位
while(USART_GetFlagStatus(pUSARTx,USART_FLAG_TXE)==RESET);//等待数据发送完毕
}
在主函数中发送十六进制数据:
Usart_SendHalfWord(USART1,0XFF56);//发送16位数据
串口助手显示的是字符,要想接收到的数据和发送的一样,需要把串口助手选择为16进制接收
串口助手接收到ff 56。虽然是16位的数据但是显示的时候还是一个字节一个字节的显示,十六进制ff是一个字节 56是一个字节
//发送一个数组数据
void Usart_SendArray(USART_TypeDef* pUSARTx,uint8_t *array,uint8_t num)
{
//每次想要发送多少数据,通过形参num传进来,num定义的是8位的,那么函数最多发送255个
int i;
for(i=0;i
判断发送一个字节的数据标志位:USART_FLAG_TXE
判断发送一连串字节的数据标志位:USART_FLAG_TC
在主函数中定义一个数组
uint8_t a[12]={1,2,3,4,5,6,7,8,9,10,98,100};
将数据内容发送出去
Usart_SendArray(USART1,a,12);
串口助手:非十六进制数据形式接收数据:1-10的ASCII是无法显示的
//发送字符串
void Usart_SendStr(USART_TypeDef* pUSARTx,uint8_t *str)//指定串口,要发送的字符串内容
{
uint8_t i=0;
//使用do-while循环,do的时候已经开始发送了
do{
//需要调用发送一个字节函数
Usart_SendByte(USART1,*(str+i));//发送一次之后指针地址后移一个
i++;
}while(*(str+i)!='\0');//最后结尾不等于'\0'为真,继续发送
//如果='\0'表示发送完毕
while(USART_GetFlagStatus(pUSARTx,USART_FLAG_TC)==RESET);//等待发送完毕
}
在主函数中调用函数发送一个字符串
Usart_SendStr(USART1,"欢迎使用stm32\n");//此时发送的是字符,串口助手要取消十六进制接收
有时候想直接用printf函数直接发送,肯定是不可以的
printf函数底层会有一个fputc,如果想要使用,需要重新定义
int fputc(int ch FILE *f)
{
//发送一个字节数据到串口
USART_SendData(USART1,(uint8_t)ch);
//等待发送完毕
while(USART_GetFlagStatus(USART1,USART_FLAG_TXE)==RESET);
//检测发送数据寄存器是否为空USART_FLAG_TXE
return (ch);
}
在主函数中编写代码
printf("串口测试实验\n");
//发送一个字符也可以直接使用putchar('a')
putchar('a');//串口助手会接收到字母a
getchar()等价于scanf()函数
如果使用getchar函数也需要重新定义
重定向c库函数scanf到串口,重写后可以使用scanf和getchar函数
重定向c库函数scanf到串口,重写后可以使用scanf和getchar函数
int fgetc(FILE *f)
{
//等待串口输入数据
/* 有了这个等待就不需要在中断中进行了 */
while(USART_GetFlagStatus(USART1,USART_FLAG_RXNE)==RESET);
return (int)USART_ReceiveData(USART1);
}
如果在主函数中使用getchar()需要把下面的中断设置代码注释掉(下图),否则会冲突
因为不需要在中断中进行
在主函数的while(1)循环中加入接收数据和发送数据的代码
ch=getchar();
printf("ch=%c\n",ch);//打印接收到的字符
用串口助手发送什么字符,串口助手就会接收到单片机返回什么字符
当接收到数据时产生中断
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);//开启中断
有中断就要设置中断优先级
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寄存器
编写中断服务函数
串口助手发送什么数据给单片机,单片机自动将接收到的数据返回给串口助手
//中断服务函数
void USART1_IRQHandler()
{
u8 ucTemp;
if(USART_GetFlagStatus(USART1,USART_IT_RXNE)!=RESET)
{
ucTemp=USART_ReceiveData(USART1);
USART_SendData(USART1, ucTemp);
}
}
当外部设备或者串口调试助手给单片机发送数据时,单片机检测到数据接收寄存器非空,表示数据来了此时产生中断,进去中断服务函数调用固件库函数这个标志位是否真正置1,以免产生误中断,如果真的产生1时,调用USART_ReceiveData(USART1)函数接收数据,把数据放ucTemp变量中
再调用USART_SendData(USART1, ucTemp)把数据发送回串口助手
通过串口接收到的数据进行控制led灯,这样子就不需要中断来接收了,通过查询方法即可,这时候要把中断部分给注释了,中断服务函数注释掉就可以了
在主函数中加入以下代码:
u8 ch;//存放电脑接收到的数据
while(1)
{
ch=getchar();//读取串口数据
printf("ch=%c\n",ch);//打印接收到的字符
switch(ch)//进行匹配
{
case '1':
LED0=0;break;//打开LED0
case '2':
LED1=0;break;//打开LED1
}
}
如果出现错误:参考此链接
解决方案-链接
使用串口助手分别发送1和2就可以控制led灯的亮灭了
前面已经说到,如果不勾选十六进制数据接收和发送的话,其他情况都是以字符的形式发送和接收的,所以此处在发送和接收数据时都要将hex选项进行勾选
由于前两个字节数据是固定的模块地址,因为可以用来当做判断标准,定义一个接收数据的协议:只有第一个字节和第二个字节都符合条件时,才将数据存储到数组中
模块数据流:2C E4 04 00 00 AD 03 38 FC
数据流共9个字节,所以先定义一个存储9个字节的八位数组
u8 table_data[9];//这是提前定义一个数组存放接收到的数据
u8 table_cp[9];//这是额外定义一个数组,将接收到的数据复制到这里面
u16 count=0;//接收数据计数
编写中断服务函数
//使用自定义协议接收十六进制数据
void USART1_IRQHandler(void) //串口1中断服务程序
{
u8 Res,i;
if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) //接收中断
{
Res =USART_ReceiveData(USART1);//(USART1->DR); //读取接收到的数据
if(count==0)//如果是接收的第一个数据
{
table_data[count]=Res;//将第一个数据存到数据中第一元素
if(table_data[0]==0x2c)//判断接收的第一个数据是不是十六进制0X2C
count++;//如果第一个数据是0X2C则表示正确计数+1
}
else if(count==1)//第一个数据接收正确的情况下,判断第二个数据
{
if(Res==0xe4)//如果刚接收的数据是0xE4则表示数据正确
{
table_data[count]=Res;//将数据储存到数组第二个元素位置
count++;//接收数据计数+1
}
else//如果第二个字符不是0XE4则计数清零,重新接收
count=0;
}
else if(count==2&&Res==0)//如果前两个数据正确,接收的第三个数据是0,则清零计数,重新接收数据
{
count=0;
}
else if(count>1&&count<9)//这是可以接收数据的范围,只要count在数据可接收数据范围内即可进行存入数据
{
table_data[count]=Res;
count++;
}
else if(count>=9)//如果接收数据超过数组大小,则清零重新接收
{
count=0;
}
}
memset(table_cp, 0, sizeof(table_data));//在使用数组table_cp时清空
for(i=0;i<9;i++)//把接收到的数据复制到table_cp数组中
{
table_cp[i]= table_data[i];
}
}
上面实现的是通过自定义协议接收并存储数据
在接收到十六进制数据之后,想要提取其中的两个字节并将其转化为十进制数据,首先需要编写一个十六进制转换函数:(输入十六进制数据返回十进制数据)
int hextoDec(int hex)
{
int sum=0,mul=1;
int i,r;
int count=0;
do{
r=hex%16;
for(i=0;i
主函数的while内容如下:
while(1)
{
if(table_cp[0]==0x2c)//如果数组第一个十六进制数据是0X2C则进行
{
//用十进制数据打印一下接收到的数据
//原始数据(十六进制数据)是2C E4 04 00 00 AD 01 23 FC
//前两位是固定的,第7个和第8个十六进制数据分别是CO2的高八位和低八位
for(i=0;i<9;i++)
{
printf(" %d \n",table_cp[i]);
}
printf("\r\n");//加一个回车换行
//把对应的十六进制数据转化为十进制数据
num_H=hextoDec(table_cp[6]);//高8位
num_L=hextoDec(table_cp[7]);//低8位
printf("hh=%d LL=%d \n",num_H,num_L);
num=num_H*256+num_L;//使用CO2浓度计算公式计算出数值
printf("CO2=%d\n",num);
}
}
验证如下:
如下图所示:使用串口助手发送十六进制数据:
2C E4 04 00 00 AD 03 38 FC
其中第7个字节是CO2的高8位,第8个字节是CO2浓度的低8位,然后再按照计算公式进行计算即可求出二氧化碳浓度
完整代码下载链接