【STM32】标准库与HAL库对照学习教程八--串口通信详解
| 本篇文章是对单片机串口通信的详讲,串口通信作为STM32单片机的一个重要功能,在程序调试中发挥着重要的作用,本篇从通信基本原理讲解开始,一步一步让您理解并会使用STM32的串口通信,实验程序包括标准库与HAL库,例程为串口中断通信实验,您也可以点击目录跳转到自己想看的内容。 |
通信的方式可以分为多种:
下面我们就来简单介绍这几种通信方式。
串行通信是指使用一条数据线,将数据一位一位地依次传输,每一位数据占据一个固定的时间长度。

并行通信通常是将数据字节的各位用多条数据线同时进行传送,通常是8位、16位、32位等数据一起传输。

异步通信是指通信的发送与接收设备使用各自的时钟控制数据的发送和接收过程。
异步通信是以字符(构成的帧)为单位进行传输,字符与字符之间的间隙(时间间隔)是任意的。


优点:不要求收发双方时钟的严格一致,实现容易。
缺点:每个字符要附加2~3位用于起止位,各帧之间还有间隔,因此传输效率不高 。
同步通信时要建立发送方时钟对接收方时钟的直接控制,使双方达到完全同步。实现方法有外同步和自同步两种。

优点:由于传输因此传输效率高。
缺点:同步实现困难且开销大。
单工是指数据传输仅能沿一个方向,就是一个设备只管发送,一个设备只管接收。

半双工是指数据传输可以沿两个方向,但需要分时进行。也就是发送的时候不能接收,接收的时候不能发送。

全双工是指数据可以同时进行双向传输。也就是发送的时候可以接收,接收的时候可以发送。

衡量通信性能的一个非常重要的参数就是通信速率,通常以比特率(Bitrate)来表示。
比特率是每秒钟传输二进制代码的位数,单位是:位/秒(bps)。
例如:每秒钟传送200个字符,而每个字符格式包含10位(1个起始位、1个停止位、8个数据位),这时的比特率为:10位×200个/秒 = 2000 bps
串口通信(Serial Communication),是指外设和计算机之间,通过数据信号线、地线等,按位进行传输数据的一种通信方式,属于串行通信方式。
串口是一种接口标准,它规定了接口的电气标准,没有规定接口插件电缆以及使用的协议。
USART即通用同步异步收发器,它能够灵活地与外部设备进行全双工数据交换。UART即通用异步收发器,它是在USART基础上裁剪掉了同步通信功能。STM32F103ZET6芯片含有3个USART,2个UART外设。

图片在STM32F1xx中文参考手册 通用同步异步收发器章节
结构图重点在框住的部分,通过寄存器USART_SR的TXE、TC、RXNE位知道串口收发情况。
TXE位是USART_SR寄存器的第七位,为1时,TDR寄存器中的数据已经被转移到移位寄存器,为0时,TDR寄存器中的数据还没有被转移到移位寄存器。复位时为1,TDR寄存器中有数据时,该位立即为0。


TC位是USART_SR寄存器的第六位,为1时,移位寄存器中的数据发送完成,为0时,移位寄存器中还有数据。复位时为1,需要手动清0,或者进行读操作让其为0。


当接收完数据时,该位为1,其他时候为0。


串口数据收发线要交叉连接,计算机的TXD要对应单片机的RXD,计算机的RXD要对应单片机的TXD,并且共GND。
如下图:

单片机与电脑进行通信需要用到USB转串口模块,因为电脑上没有RXD、TXD引脚,并且电脑还要安装CH340的驱动。
市场常见的USB转串口模块:

接上电脑,TX接单片机的RX,RX接单片机的TX,电源与地接到单片机上就可以串口通信了。
电脑上的CH340驱动我放在网盘上了,有需要自己去下载。
https://pan.baidu.com/s/1bO7mpkwjkB19HXvmD0083Q
密码:kpa6
(1)使能串口时钟及GPIO端口时钟
(2)GPIO端口模式设置,设置串口对应的引脚为复用功能
(3)初始化串口参数,包含波特率、字长、奇偶校验等参数
(4)使能串口
(5)设置串口中断类型并使能
(6)设置串口中断优先级,使能串口中断通道
(7)编写串口中断服务函数
(1)复制上一章的工程,并重命名为8、串口中断通信。

(2)进入工程文件,进入APP文件,新建USART文件夹用来存放与串口相关的文件。

(3)打开工程,新建文件,并命名为usart.h与usart.c。
①

②

(4)添加文件到目录,并添加头文件路径。
①

②

(5)要使用串口需要添加相应的文件。
①

②

mian.c
#include "LED.h"
#include "Delay.h"
#include "System.h"
#include "usart.h"
/*************************************************
*函数名: main
*函数功能: 主函数
*输入: 无
*返回值: 无
**************************************************/
int main()
{
SysTick_Init(72);
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //抢占式优先级与响应式优先级的分组
LED_Init();
USART1_Init(9600);
while(1)
{
}
}
usart.h
#ifndef USART_H_
#define USART_H_
#include "stm32f10x.h"
/************串口引脚************/
#define USART1_GPIO_Port GPIOA
#define USART1_RX_Pin GPIO_Pin_10
#define USART1_TX_Pin GPIO_Pin_9
/************串口函数************/
void USART1_Init(u32 bound); //串口初始化
void USART_SendBit(USART_TypeDef* USARTx,u16 Data); //发送单个数据
uint16_t USART_ReceiveBit(USART_TypeDef* USARTx); //接收单个数据
void USART_SendString(USART_TypeDef* USARTx,char *string); //发送字符串
#endif
usart.c
#include "usart.h"
/*************************************************
*函数名: USART1_Init
*函数功能: 串口1的初始化
*输入: bound:波特率
*返回值: 无
**************************************************/
void USART1_Init(u32 bound)
{
GPIO_InitTypeDef GPIO_InitStruct;
USART_InitTypeDef USART_InitStruct;
NVIC_InitTypeDef NVIC_InitStruct;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_USART1, ENABLE); //时钟使能
GPIO_InitStruct.GPIO_Pin = USART1_TX_Pin; //发送引脚
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽输出
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz; //发送速度
GPIO_Init(USART1_GPIO_Port, &GPIO_InitStruct); //引脚初始化
GPIO_InitStruct.GPIO_Pin = USART1_RX_Pin; //接收引脚
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IN_FLOATING; //浮空输入
GPIO_Init(USART1_GPIO_Port, &GPIO_InitStruct); //引脚初始化
USART_InitStruct.USART_BaudRate = bound; //波特率
USART_InitStruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None; //无硬件数据流控制
USART_InitStruct.USART_Mode = USART_Mode_Rx|USART_Mode_Tx; //收发模式
USART_InitStruct.USART_Parity = USART_Parity_No; //没有校验位
USART_InitStruct.USART_StopBits = USART_StopBits_1; //一位停止位
USART_InitStruct.USART_WordLength = USART_WordLength_8b; //8位一个字节
USART_Init(USART1, &USART_InitStruct); //初始化串口
USART_Cmd(USART1, ENABLE); //串口使能
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); //接收中断使能
NVIC_InitStruct.NVIC_IRQChannel = USART1_IRQn; //要打开的中断通道
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 2; //抢占式优先级
NVIC_InitStruct.NVIC_IRQChannelSubPriority = 2; //相应式优先级
NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE; //NVIC通道使能
NVIC_Init(&NVIC_InitStruct);
USART_ClearFlag(USART1, USART_FLAG_TC); //TC位初始值位1,要先清0
}
//直接使用串口发送接收函数会出现内容覆盖的问题,所以需要我们重写函数
/*************************************************
*函数名: USART_SendBit
*函数功能: 串口发送函数
*输入: Data:发送的数据
*返回值: 无
**************************************************/
void USART_SendBit(USART_TypeDef* USARTx,u16 Data)
{
USART_SendData(USARTx, Data);
//while(!USART_GetFlagStatus(USARTx, USART_FLAG_TXE)); //要等待数据全部转到移位寄存器
//USART_ClearFlag(USARTx, USART_FLAG_TXE); //清空标志位
while(!USART_GetFlagStatus(USARTx, USART_FLAG_TC));//要等待数据全部发出
USART_ClearFlag(USARTx, USART_FLAG_TC); //清空标志位
}
/*************************************************
*函数名: USART_ReceiveBit
*函数功能: 串口接收函数
*输入: USARTx:串口
*返回值: 接收到的数据
**************************************************/
uint16_t USART_ReceiveBit(USART_TypeDef* USARTx)
{
while(!USART_GetFlagStatus(USARTx, USART_FLAG_RXNE)); //等待接收的数据全部接收
USART_ClearFlag(USARTx, USART_FLAG_RXNE);
return USART_ReceiveData(USARTx);
}
/*************************************************
*函数名: USART_SendString
*函数功能: 串口发送字符串函数
*输入: USARTx:串口,string:字符型指针
*返回值: 无
**************************************************/
void USART_SendString(USART_TypeDef* USARTx,char *string)
{
while(*string)
{
USART_SendBit(USARTx,*string++);
}
}
/*************************************************
*函数名: USART1_IRQHandler
*函数功能: 串口中断函数-将从电脑发送的数据发回给电脑
*输入: 无
*返回值: 无
**************************************************/
void USART1_IRQHandler()
{
u16 r;
if(USART_GetITStatus(USART1, USART_IT_RXNE) == SET) //判断中断标志
{
r = USART_ReceiveData(USART1);
USART_SendBit(USART1,r);
}
}

(1) 打开cubemx,新建工程,选择自己的芯片。

(2) 配置RCC,选择外部高速时钟。

(3) 配置时钟树。

(4) 配置串口
①


(5) 工程文件配置并生成工程
①

②

相关参数:
回调函数:
串口中断接收完成之后,会进入该函数,该函数为空函数,用户需自行修改。

#include "string.h"
uint8_t Rx_String[100]; //接收字符串数组
uint8_t Rx_Flag=0; //接收字符串计数
uint8_t Rx_buff; //接收缓存

HAL_UART_Receive_IT(&huart1, (uint8_t *)&Rx_buff, 1); //开启接收中断

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if(huart == &huart1)
{
Rx_String[Rx_Flag++] = Rx_buff; //接收字符
if(Rx_String[Rx_Flag-1] == 0x0A) //判断是否接收结束
{
HAL_UART_Transmit(&huart1, (uint8_t *)&Rx_String, Rx_Flag,0xFFFF); //字符串发送
while(HAL_UART_GetState(&huart1) == HAL_UART_STATE_BUSY_TX); //判断发送是否完毕
memset(Rx_String,0x00,sizeof(Rx_buff)); //清空接收字符串
Rx_Flag = 0; //清空计数器
}
HAL_UART_Receive_IT(&huart1, (uint8_t *)&Rx_buff, 1); //再开启接收中断
}
}

C语言中printf函数默认输出设备是显示器,如果要实现在
串口或者LCD上显示,必须重定义标准库函数里调用的与输出设备相关的函数。比如使用printf输出到串口,需要将fputc里面的输出指向串口。
这一过程就叫重定向。
对于标准库
将这段程序加入主函数中
int fputc(int ch,FILE *p) //函数默认的,在使用printf函数时自动调用
{
USART_SendData(USART1,(u8)ch);
while(USART_GetFlagStatus(USART1,USART_FLAG_TXE)==RESET);
return ch;
}
int fgetc(FILE *p)
{
uint8_t ch = 0;
USART_SendData(USART1, ch);
return ch;
}
并添加stdio.h头文件,就可以使用C语言中的printf函数将字符串通过串口打印在电脑上了。
对于HAL库
将这段程序加入主函数中
/**
* 函数功能: 重定向c库函数printf到DEBUG_USARTx
* 输入参数: 无
* 返 回 值: 无
* 说 明:无
*/
int fputc(int ch, FILE *f)
{
HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xffff);
return ch;
}
/**
* 函数功能: 重定向c库函数getchar,scanf到DEBUG_USARTx
* 输入参数: 无
* 返 回 值: 无
* 说 明:无
*/
int fgetc(FILE *f)
{
uint8_t ch = 0;
HAL_UART_Receive(&huart1, &ch, 1, 0xffff);
return ch;
}
并添加stdio.h头文件,就可以使用C语言中的printf函数将字符串通过串口打印在电脑上了。
串口通信的接口标准有很多,有RS-232C、RS-232、RS-422A、RS-485等。常用的就是RS-232和RS-485。RS-232其实是RS-232C的改进,原理是一样的。这里我们就以RS-232C接口进行讲解。
RS-232C是EIA(美国电子工业协会)1969年修订RS-232C标准。RS-232C定义了数据终端设备(DTE)与数据通信设备(DCE)之间的物理接口标准。
RS-232C接口规定使用25针连接器,简称DB25,连接器的尺寸及每个插针的排列位置都有明确的定义 。

RS-232C对逻辑电平也做了规定,如下:
由此可见,RS-232C是用正负电压来表示逻辑状态,与晶体管-晶体管逻辑集成电路(TTL)以高低电平表示逻辑状态的规定正好相反
到这里就结束啦!

我想使用spawn(针对多个并发子进程)在Ruby中执行一个外部进程,并将标准输出或标准错误收集到一个字符串中,其方式类似于使用Python的子进程Popen.communicate()可以完成的操作。我尝试将:out/:err重定向到一个新的StringIO对象,但这会生成一个ArgumentError,并且临时重新定义$stdxxx会混淆子进程的输出。 最佳答案 如果你不喜欢popen,这是我的方法:r,w=IO.pipepid=Process.spawn(command,:out=>w,:err=>[:child,:out])
我正在尝试找到一种方法来规范化字符串以将其作为文件名传递。到目前为止我有这个:my_string.mb_chars.normalize(:kd).gsub(/[^\x00-\x7F]/n,'').downcase.gsub(/[^a-z]/,'_')但第一个问题:-字符。我猜这个方法还有更多问题。我不控制名称,名称字符串可以有重音符、空格和特殊字符。我想删除所有这些,用相应的字母('é'=>'e')替换重音符号,并将其余的替换为'_'字符。名字是这样的:“Prélèvements-常规”“健康证”...我希望它们像一个没有空格/特殊字符的文件名:“prelevements_routin
文章目录1.开发板选择*用到的资源2.串口通信(个人理解)3.代码分析(注释比较详细)1.主函数2.串口1配置3.串口2配置以及中断函数4.注意问题5.源码链接1.开发板选择我用的是STM32F103RCT6的板子,不过代码大概在F103系列的板子上都可以运行,我试过在野火103的霸道板上也可以,主要看一下串口对应的引脚一不一样就行了,不一样的就更改一下。*用到的资源keil5软件这里用到了两个串口资源,采集数据一个,串口通信一个,板子对应引脚如下:串口1,TX:PA9,RX:PA10串口2,TX:PA2,RX:PA32.串口通信(个人理解)我就从串口采集传感器数据这个过程说一下我自己的理解,
我经常迷上ruby的一件事是递归模式。例如,假设我有一个数组,它可能包含无限深度的数组作为元素。所以,例如:my_array=[1,[2,3,[4,5,[6,7]]]]我想创建一个方法,可以将数组展平为[1,2,3,4,5,6,7]。我知道.flatten可以完成这项工作,但这个问题是作为我经常遇到的递归问题的一个例子-因此我试图找到一个更可重用的解决方案。简而言之-我猜这种事情有一个标准模式,但我想不出任何特别优雅的东西。任何想法表示赞赏 最佳答案 递归是一种方法,它不依赖于语言。您在编写算法时要考虑两种情况:再次调用函数的情
我正在使用ruby标准记录器,我想要每天轮换一次,所以在我的代码中我有:Logger.new("#{$ROOT_PATH}/log/errors.log",'daily')它运行完美,但它创建了两个文件errors.log.20130217和errors.log.20130217.1。如何强制它每天只创建一个文件? 最佳答案 您的代码对于长时间运行的应用程序是正确的。发生的事情是您在给定的一天多次运行代码。第一次运行时,Ruby会创建一个日志文件“errors.log”。当日期改变时,Ruby将文件重命名为“errors.log
如何获取外部命令的输出并从中提取值?我有这样的东西:stdin,stdout,stderr,wait_thr=Open3.popen3("#{path}/foobar",configfile)if/exit0/=~wait_thr.value.to_srunlog.puts("Foobarexitednormally.\n")puts"Testcompleted."someoutputvalue=stdout.read("TX.*\s+(\d+)\s+")puts"Outputvalue:"+someoutputvalueend我没有在标准输出上使用正确的方法,因为Ruby告诉我它不能
我遇到了同样的问题here对于python,但对于ruby。我需要输出这样一个小数字:0.00001,而不是1e-5。有关我的特定问题的更多信息,我正在使用f.write("Mynumber:"+small_number.to_s+"\n")输出到一个文件对于我的问题,准确性不是什么大问题,所以只做一个if语句来检查是否small_number那么更通用的方法是什么? 最佳答案 f.printf"Mynumber:%.5f\n",small_number您可以将.5(小数点右侧5位数字)替换为您喜欢的任何特定格式大小,例如,%8
一、什么是MQTT协议MessageQueuingTelemetryTransport:消息队列遥测传输协议。是一种基于客户端-服务端的发布/订阅模式。与HTTP一样,基于TCP/IP协议之上的通讯协议,提供有序、无损、双向连接,由IBM(蓝色巨人)发布。原理:(1)MQTT协议身份和消息格式有三种身份:发布者(Publish)、代理(Broker)(服务器)、订阅者(Subscribe)。其中,消息的发布者和订阅者都是客户端,消息代理是服务器,消息发布者可以同时是订阅者。MQTT传输的消息分为:主题(Topic)和负载(payload)两部分Topic,可以理解为消息的类型,订阅者订阅(Su
LL库和HAL库简介LL:Low-Layer,底层库HAL:HardwareAbstractionLayer,硬件抽象层库LL库和hal库对比,很精简,这实际上是一个精简的库。LL库的配置选择如下:在STM32CUBEMX中,点击菜单的“ProjectManager”–>“AdvancedSettings”,在下面的界面中选择“AdvancedSettings”,然后在每个模块后面选择使用的库总结:1、如果使用的MCU是小容量的,那么STM32CubeLL将是最佳选择;2、如果结合可移植性和优化,使用STM32CubeHAL并使用特定的优化实现替换一些调用,可保持最大的可移植性。另外HAL和L
TCL脚本语言简介•TCL(ToolCommandLanguage)是一种解释执行的脚本语言(ScriptingLanguage),它提供了通用的编程能力:支持变量、过程和控制结构;同时TCL还拥有一个功能强大的固有的核心命令集。TCL经常被用于快速原型开发,脚本编程,GUI和测试等方面。•实际上包含了两个部分:一个语言和一个库。首先,Tcl是一种简单的脚本语言,主要使用于发布命令给一些互交程序如文本编辑器、调试器和shell。由于TCL的解释器是用C\C++语言的过程库实现的,因此在某种意义上我们又可以把TCL看作C库,这个库中有丰富的用于扩展TCL命令的C\C++过程和函数,所以,Tcl是