前导:本文的目的与,意在于面向应用的学习单片机,故不会涉及太多的原理知识,例如寄存器之类的。
主要目的在于面向应用的学习单片机,学会单片机的基础用法,开发板采取野火的指南者f103。
作者大二小白,写的不好的地方轻点喷,欢迎评论区交流
全部工程代码开源在Gitee仓库
文章目录
1 串口通信
串口通信是一种设备间非常常用的异步串行通行方式,其简单便捷,大部分电子设备都支持。
1.1 通信简介
通信的概念很好理解,大家之间互相对话,qq,微信在网上的消息传输,短信都是通信的一部分。本质上就是为了两个实体之间的互相交流。单片机自然也有很多需要通信的情况,通信的方法也有许多种
1.2 电子设备通信的几个概念
1.2.1 同步和异步通信
同步通信就是双方按照同一个时钟节拍工作,发送的每一个消息每一个数据都是实时传送给对方,对方实时接收的,对方如果没接收就会一直等待,直到对方接收为止。例如我们的微信通话,电话,别人接了之后我们就建立了同步通信。
异步通信就是指双方通信的频率不固定,发送方发送一个消息后,不必等待对方的接收,可以继续发送消息。
1.2.2 并行和串行
-
串行通信指的是, 一条信息的各位数据被逐位按顺序传送,最少的情况下只需要一根传输线就可以完成任务,但传输速度较慢。串行通讯又分为异步通讯和同步通讯两种方式。在单片机中,主要使用异步通讯方式。
串行通讯中,两个设备之间通过一对信号线进行通讯,其中一根为信号线,另外一根为信号地线,信号电流通过信号线到达目标设备,再经过信号地线返回,构成一个信号回路。
-
并行通信指的是,用了比串行通信更多的信号线路,例如8根,16根,32根,可以更快速的传输数据,但是复杂度较高,成本较高。并且抗干扰能力较低。
1.2.3 单工,半双工 ,全双工
- 单工
单工就是指A只能发信号,而B只能接收信号,通信是单向的。 - 半双工
经典的早期的对讲机就是半双工模式,双方都可以发送和接收,但是发送的时候只能发送,接收的时候只能接收 - 全双工
指交换机在发送数据的同时也能够接收数据,两者同步进行,这好像我们平时打电话一样,说话的同时也能够听到对方的声音。目前的交换机都支持全双工。
1.3 串口通信相关名词
1.3.1 波特率
由于异步通信中,双方没有同步的时钟信号,所以两个设备之间需要约定好波特率,波特率也指的是串口通信的速率,也就是串口通信时每秒钟可以传输多少个二进制位,因此波特率最好是大家熟知的,约定俗成的一般会用9600或者115200。
1.3.2 起始位、数据位、奇偶校验位、停止位
1.4 总结
串口通信时因为是异步通信,所以通信双方必须事先约定好通信参数,这些通信参数包括:波特率、数据位、校验位、停止位(串口通信中起始位定义是唯一的,所以一般不用选择)。
2 STM32 USART
2.1简介
USART 通用同步异步收发器是一个串行通信设备,可以灵活的与外部设备进行全双工数据交换。有别与USART,还有一个UART,它在USART基础上裁剪掉了同步通信功能,只有异步通信。我们平时用的串口通信基本都是 UART。
2.2框图
下面是STM32F103VET6芯片的USART引脚
USART1的时钟来源于APB2总线时钟,最大频率为72MHZ,其他4个时钟来源于APB1总线时钟,最大频率36MHZ。UART只有异步传输功能,没有SCLK、nCTS和nRTS功能引脚。
3 USART配置部分
3.1 配置分析
- 使能时钟
- 配置GPIO
- 配置中断
- 配置初始化结构体并初始化
- 开启串口接收中断,使能
3.2 代码配置部分
查询原理图,发现usb绑定的是PA10,PA9
复制就行啦。然后注释看一看,因为串口的配置基本是死的。
注意点
- 波特率那一片的配置以及GPIO的模式配置
- USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);//开启串口接中断
void USART_Config(int bound)
{
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
//使能USART1,GPIOA时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1|RCC_APB2Periph_GPIOA, ENABLE);
//USART1_TX GPIOA.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);
//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);
//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
}
4 代码收发部分
4.1 串口发送
从stm32f10x_usart.h里我们可以找到函数USART_SendData,查阅手册可得这个函数的作用是按字节发送数据。那如果我们要发现字符串就要循环一个个发。
然后每次发的时候肯定要判断是不是到了结尾。有这样的一行函数
FlagStatus USART_GetFlagStatus(USART_TypeDef* USARTx, uint16_t USART_FLAG)
看这个英语可以得到,我们需要的第二个参数是USART_FLAG_TXE
写出这样的函数,这样我们就可以实现向电脑数据的发送了。
void USART_SendString(int8_t *str)
{
uint8_t index = 0;
do
{
USART_SendData(USART1,str[index]);
while(USART_GetFlagStatus(USART1,USART_FLAG_TXE) == RESET);
index++;
}
while(str[index] != 0);
}
然后我们在while(1)前面写下这个测试
//...省略其他配置
USART_Config(9600);
USART_SendString("Hello world\n");
while(1)
{
//...省略
}
电脑成功接收到了数据(注意这里的串口配置要和你初始化的串口配置一样)
这个串口助手也已经上传到了Gitee仓库里,里面那个firetool的压缩包就是了
4.2串口接收
首先复制中断服务函数,老地方startup.s里复制,然后直接复制这些代码
我们收到数据后,肯定要先清除中断标志位,基本所有的中断程序都有这个套路。然后调用USART_ReceiveData获取接收的字符,根据该字符判断接收是否结束。
肯定要规定数据的结束规则,我这里的规则就是 以感叹号结尾或者大于20个字符。否则数据永远无法接收完整。
接收完后,标志位置1,这样就可以在主函数那边写个if判断,当RXOVER==1的时候,代表接收完成,可以进行对应操作。
char USART_RXBUF[28];
uint8_t RXOVER = 0;
uint8_t RXCUNT = 0;
void USART1_IRQHandler(void)
{
uint8_t temp;
//判断是否接收
if(USART_GetITStatus(USART1,USART_IT_RXNE) != RESET)
{
//清除中断标志位
USART_ClearITPendingBit(USART1,USART_IT_RXNE);
//获取接收的字符
temp = USART_ReceiveData(USART1);
//判断是不是停止字符
if(temp == '!' || RXCUNT==20)
{
//字符数清零
RXCUNT = 0;
//结束标志置1
RXOVER = 1;
//关闭中断
USART_ITConfig(USART1,USART_IT_RXNE,DISABLE);
}
else
{
//往接收数值中拼接数据
USART_RXBUF[RXCUNT] = temp;
RXCUNT++;
}
}
}
例子,记得收到后要RXOVER改回0,然后重新开启接收中断
while(1)
{
if(RXOVER==1)
{
//不能打印 \n 换行符
ILI9341_DispStringLine_EN(LINE(1),USART_RXBUF);
USART_ITConfig(USART1,USART_IT_RXNE,ENABLE);//开启串口接中断
RXOVER=0;
}
}
发送数据,以感叹号结尾,这时屏幕上就能显示我们发送的字符了。
5 小实验
5.1 串口发送数据控制灯泡
5.1.1 需求
串口发送数据 LED:x(换行结尾)的数据给单片机。默认亮黄灯
或者SPEED:xxx(换行结尾)(三位数) 控制闪烁的间隔速度。默认500ms
规定x=1的时候亮红灯, x=2的时候亮绿灯, x=3的时候交替闪烁当前颜色的灯,再次接受到x=3的时候则切换模式。x=其他数值的时候灯泡熄灭
发送格式错误的时候,屏幕输出 ”ERROR“
5.1.2 思路分析
-
利用一个变量color记录当前的颜色
-
解析串口的前三位是否是LED:或者SPEED:,如果不是的话,屏幕打印ERROR
-
如果是LED的话取出后面的数字,改变color变量进而改变灯的颜色
-
如果是SPEED的话取出后面的数字,改变闪烁的speed变量
-
将数字和标志位关联到一起
-
x=3的时候,以规定的speed闪烁当前color变量
5.1.3代码部分
解析数据
void Usart_Control()
{
if(RXOVER==1)
{
//调用数据解析函数
Data_Analysis();
USART_ITConfig(USART1,USART_IT_RXNE,ENABLE);//开启串口接中断
RXOVER=0;
}
}
void Data_Analysis()
{
//如果是LED的控制数据
if(USART_RXBUF[0]=='L' && USART_RXBUF[1]=='E' && USART_RXBUF[2]=='D'
&& USART_RXBUF[3]==':')
{
error_flag = 0;
if(USART_RXBUF[4]='1')
{
color = LED_RED;
}
else if(USART_RXBUF[4]='2')
{
color = LED_GREEN;
}
else if(USART_RXBUF[4]='3')
{
mode = !mode;
}
else
{
color = LED_OFF;
}
}
//如果是闪烁速度的控制数据
else if(USART_RXBUF[0]=='S'&&USART_RXBUF[1]=='P'&&USART_RXBUF[2]=='E'
&&USART_RXBUF[3]=='E'&&USART_RXBUF[4]=='D'&&USART_RXBUF[5]==':')
{
error_flag = 0;
speed = (USART_RXBUF[6]-'0')*100 +
(USART_RXBUF[7]-'0')*10 +
(USART_RXBUF[8]-'0');
}
else
{
//格式错误 标志位置1
error_flag = 1;
}
}
LED控制部分
void LED_Show()
{
if(mode)
{
LED_Color(color);
}
else
{
LED_Flashing(color,speed);
}
}
显示部分
void LCD_Show()
{
if(error_flag)
{
ILI9341_DispStringLine_EN(LINE(1),"ERROR");
}
else
{
ILI9341_DispStringLine_EN(LINE(1)," ");
}
}
5.2 串口实现倒计时
5.2.1需求
串口发送数据格式为 xx:xx:xx (换行结尾)的消息数据。
比如 01:11:15,单片机接收到后,显示出来,然后开始倒计时1小时11分钟15s,时间到的时候,蜂鸣器响,红灯常亮。按下KEY1按键后,灯泡变绿,蜂鸣器停止。
设备默认倒计时为五分钟,按下KEY1后恢复倒计时五分钟。
5.2.2思路分析
- 利用一个模式int mode标志位:用来控制倒计时到了,和未到的时候。
- 时分秒全为0的时候标志位置0,亮红灯,响蜂鸣器
- 按键按下时,亮绿灯,关蜂鸣器,时分秒置为默认,标志位置1
- systick实现秒的控制,只有标志位为1的时候才可以计时
- 串口接收到数据的时候,亮绿灯,关蜂鸣器,时分秒设置为串口发送的,标志位置1
5.2.3代码
倒计时部分
void Time_Control()
{
//时间到
if(minute==0 && second==0 && hour==0)
{
LED_Color(LED_RED);//红灯
Beep_Control(BEEP_ON);//蜂鸣器响
mode=0;
}
if(second==0 && minute>0)
{
second = 59;
minute--;
}
//注意这个hour>0 细节问题
if(minute==0 && hour>0)
{
second = 59;
hour--;
}
}
按键控制部分
void KEY_Control()
{
char key = KEY_Scan();
if(key=='1')
{
hour = 0;
second = 0;
minute = 5;
mode = 1;
Beep_Control(BEEP_OFF);
LED_Color(LED_GREEN);
}
}
Usart控制部分
这里涉及到字符转int类型的数字,ascii码值相关的。详情可以查阅ascii码表。
大家只要记住就像我代码部分这样转换就行。然后我们十位数部分*10,个位数直接加上去。
取值就是从接收数组上取值
void Usart_Control()
{
if(RXOVER==1)
{
hour = (USART_RXBUF[0] - '0')*10+(USART_RXBUF[1] - '0');
minute = (USART_RXBUF[3] - '0')*10+(USART_RXBUF[4] - '0');
second = (USART_RXBUF[6] - '0')*10+(USART_RXBUF[7] - '0');
mode=1;
Beep_Control(BEEP_OFF);
LED_Color(LED_GREEN);
USART_ITConfig(USART1,USART_IT_RXNE,ENABLE);//开启串口接中断
RXOVER=0;
}
}
LCD显示
void LCD_Show()
{
sprintf(dispBuff,"%2d:%2d:%2d", hour,minute,second);
ILI9341_DispStringLine_EN(LINE(1),dispBuff);
}
SysTick中断
extern int second;
extern int mode;
int count=0;
void SysTick_Handler(void)
{
if(++count==1000 && mode==1)
{
second--;
count=0;
}
TimeDelay--;
}
主函数
//...省略配置
while(1)
{
Usart_Control();
LCD_Show();
Time_Control();
KEY_Control();
}