STM32串口与printf函数的使用方法
1.数据通信的基本知识
1.串行/并行通信
通信目的:将一个设备的数据传送到另外一个设备,扩展硬件系统
2.单工/半双工/全双工通信
类似于【广播 对讲 电话】
不是有两根线就是全双工,而是输入和输出都有对应的数据线。
3.同步/异步通信
区分同步/异步通信的根本:判断是否有时钟信号(时钟线)。如果有时钟线则是同步通信,如果没有时钟线则是异步通信。
同步靠时钟线,异步靠比特率
4.波特率 VS 比特率
5.常见的串行通信接口
2.串口(RS-232)
1.什么是串口
实际上就是按位来对数据进行发送和接收。
2.RS232电平和CMOS/TTL电平对比
TTL/COMS和RS232传输距离基本上十几米—>抗干扰能力差
RS485(差分信号)传输距离长—>抗干扰能力强
像单片机这种小型设备都使用TTL电平
3.设备间的RS232通信示意图
MAX323:将RS232电平—》TTL/CMOS电平
DB9接口就是COM接口
4.STM32串口与电脑USB口通信示意图
USB/串口转换电路CH340C:USB电平—》TTL/CMOS电平
5.RS232异步通信协议
注意:异步通信协议中是没有使用到时钟线(SCLK)
1)起始位:标志一个数据帧开始,固定为低电平
2)停止位:用于数据帧间隔,固定为高电平
1.通讯的起始和停止信号
串口通讯的一个数据包从起始信号开始,直到停止信号结束。数据包的起始信号由一个逻辑0的数据位表示,而数据包的停止信号可由0.5、1、1.5或2个逻辑1的数据位表示,只要双方约定一致即可。
2.数据校验
3.起始位侦察
1)因为我们在电平发生变化时,可能是其他因素造成的,所以我们要对信号输入进行检测,防止误差过大。
2)因为起始位的第8,9次采样处于正中间,应该属于最稳定,最适合检测的状态,所以在后面的数据位中我们都是在数据帧的正中间对数据进行采样
4.检测噪音的数据采样
在接收数据时可能会受到噪声的影响。正常情况来说,我们是不会受到其他影响。检测3次数据要么全为1或者全为0。如果结果不全一样,就进行2:1来计算。
如果受到影响会将NE位置1,通知设备告知其此时会受到噪音影响
3.STM32USART
1.STM32USART简介
USART虽然是同步和异步都可以进行使用,但是我们大多数情况下使用的都是异步通信。
synchronous:同步
asynchronous:异步
两个USART之间不能实现同步的串口通信
2.STM32USART主要特征
3.在选型手册中查看USART/UART对应的引脚定义
4.STM32F1的USART框图
1.引脚说明
SCLK:可以用来兼容其他通信协议
2.接收/发送数据的存放/处理位置
1)数据要先放在DR寄存器中,然后再通过DR寄存器操作CPU
2)实际上RDR和TDR是同一个寄存器(地址一样),但是实际硬件中,分为两个寄存器。
3)TDR/RDR和发送/接收移位寄存器之间相互合作,使得数据帧之间没有间隙(当TDR将数据传输到发送移位寄存器后,马上将新的数据放入TDR)–>效率高
3.波特率的处理
5.框图简化版
4.设置USART/UART波特率
一般情况下:波特率=比特率
1.计算公式
baud:波特率是用户自己定义的,然后求出USARTDIV,然后分为整数部分和小数部分,在存储到USART_BRR寄存器
16的含义:因为我们在内部对一个bit位,分为16次进行采样。
2.波特比率寄存器(BRR)
3.举个例子
在线进制转换 | 进制转换器 — 在线工具 (sojson.com)
4.公式推导
因为小数部分和整数部分都要往左移动4位,所以将整个值*16
5.USART寄存器
USART_DR包含了已发送的数据或者接收到的数据。USART_DR实际是包含了两个寄存器,一个专门用于发送的可写TDR,一个专门用于接收的可读RDR。当进行发送操作时,往USART_DR写入数据会自动存储在TDR内;当进行读取操作时,向USART_DR读取数据会自动提取RDR数据。
TDR和RDR都是介于系统总线和移位寄存器之间。串行通信是一个位一个位传输的,发送时把TDR内容转移到发送移位寄存器,然后把移位寄存器数据每一位发送出去,接收时把接收到的每一位顺序保存在接收移位寄存器内然后才转移到RDR。
1.控制寄存器 1(USART_CR1)
2.控制寄存器 2(USART_CR2)
3. 控制寄存器 3(USART_CR3)
4. 数据寄存器(USART_DR)
1)具体传输多少位取决于寄存器USART_CR1中的位12M【字长】
2)设置好控制和波特率寄存器后,往该寄存器写入数据即可发送,接收数据则读该寄存器
5.状态寄存器(USART_SR)
根据TC位(发送完成位)可以知道能否发数据,根据RXNE位(读数据寄存器非空)知道是否收到数据。
6.需要配置的时序总结
6.USART/UART异步通信配置步骤
1.HAL_USART_Init
对USART进行初始化
2.HAL_USART_Receive_IT
USART的中断使能函数
这个函数是非阻塞式的,没有执行完也可以出来
3.HAL_USART_Receive
没有开启中断的UASRT
这个函数是阻塞式的,没有执行完不可以出来
7.通过串口接收或者发送一个字符
发送只能发送十六进制,不能发送中文
1.连接注意点
2.原理图分析
3.代码编写
usart_init()
相关的设置参数和开启usart的中断
hal_usart_mspinit()
1)使能USART1和对应的IO时钟
2)初始化IO
3)使能USART1中断,设置NVIC优先级
usart1_IRQHandler
中断回调服务函数
2)HAL_UART_iRQHandler()会清除中断
HAL_USART_RxCpltCallback
串口接收数据的回调函数
8.通过串口发送一个字符(库函数)
1.初始化串口
1)打开时钟【串口,GPIO】—>RCC_APB2PeriphClockCmd
2)初始化串口,GPIO—>相关结构体的设置
3)使能串口
2.发送数据:单个字符
1)我们将数据传输出去(从TDR传输到DR寄存器)需要时间,所以我们需要判断标志位,看其是否已经发送完毕
3.发送数据:数组
4.发送数据:字符串
5.发送数据:数字
将数字一个一个拆开,组成数字字符传输出去
9.数据模式
1.CubeMX和HAL库的串口实战
1、CubeMX中打开并设置串口
1.设置对应的RCC
2.查看原理图上对应的RX和TX
注意点:我们的串口对应的应该是RX–》TX,TX–》RX
我们要先查看原理图中是否已经帮我们接反了,如果没有则需要我们自己手动接反。
我们使用的STM32F103C8xx直接接入USB口即可
由上面分析可知,我们使能PA9和PA10即可
3.设置相关的USART
4.设置相关的时钟
5.在usart.c文件中添加输入/输出的函数
在使用之前记得先定义一个宏定义
#include<stdio.h>
“PUTCHAR_PROTOTYPE’’的含义
PUTCHAR_PROTOTYPE
是一个在使用HAL库时,用户可以定义的宏,通常用于重定向printf
函数的输出。这个宏的目的是提供一个适配不同硬件平台的接口,以实现printf
输出到用户定义的设备或串口。
#ifdef __GNUC__//当前在Linux系统下
#define PUTCHAR_PROTOTYPE int __io_putchar(int ch)
#else//在windows下
#define PUTCHAR_PROTOTYPE int fputc(int ch, FILE *f)
#endif
/**
* @brief Retargets the C library printf function to the USART.
* @param None
* @retval None
*/
//PUTCHAR_PROTOTYPE 宏是一个用户在使用HAL库时可以自定义的宏,
//用于实现 printf 函数的输出重定向。
PUTCHAR_PROTOTYPE
//int fputc(int ch, FILE *f)
{
/* Place your implementation of fputc here */
/* e.g. write a character to the EVAL_COM1 and Loop until the end of transmission */
HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xFFFF);
return ch;
}
6.添加中断处理函数
要自己定义receive_char【uint8_t】–》暂存和处理串口接收到的字节内容
//rc用来暂存和处理串口接收到的字节内容的
uint8_t receive_char;
//当我们在串口中发送一个字符串的时候就会进入中断
HAL_UART_Receive_IT(&huart1, &receive_char, 1);
while(1);
//这个就是HAL库对接的接收中断处理程序
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
//判断是否为usart1
if(huart->Instance == USART1)
{
//这里就是真正的中断处理代码
//我们这里的处理就是接收到一个字节后原封不动的发回去
HAL_UART_Transmit(&huart1, &receive_char, 1,0xFF);//发送字符
//等待本次接收完毕,同时开启下一次接收【更新receive_char】
//HAL_UART_Receive_IT:开启中断处理流程
while(HAL_UART_Receive_IT(&huart1, &receive_char, 1) != HAL_OK);
}
}
由于要先触发一次中断才可以进入,则我们要在初始化部分先进行一次中断
2、串口操作
(1)阻塞模式串口发送:CPU不做其他事情
MCU的CPU一个字节一个字节的将要发送的内容丢给串口模块,然后看着串口模块将这个字节发送出去,然后CPU再去拿下一个字节来丢给串口模块。直到本次要发送的所有字节全部发完,CPU才会去做其他事。
(2)中断模式串口发送:CPU轮询式查询
MCU的CPU向串口模块丢一个字节,然后串口模块慢慢发,CPU丢完这个字节后会跳出去做其他事情,等串口模块发完这个字节后会生成一个中断,中断会通知CPU过来继续丢下一个字节。
2.源码分析和串口发送的实现
1、阻塞式串口发送/接收
1.1 阻塞式发送:HAL_UART_Transmit(常用)
HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout)
{
uint16_t *tmp;
uint32_t tickstart = 0U;
/* Check that a Tx process is not already ongoing */
//查看状态
if (huart->gState == HAL_UART_STATE_READY)
{
if ((pData == NULL) || (Size == 0U))
{
return HAL_ERROR;
}
/* Process Locked */
//将串口模块锁住了,将变量的值设置为"HAL_LOCKED"
__HAL_LOCK(huart);
huart->ErrorCode = HAL_UART_ERROR_NONE;
huart->gState = HAL_UART_STATE_BUSY_TX;
/* Init tickstart for timeout managment */
//获取当前的时候,用于判断是否超时
tickstart = HAL_GetTick();
huart->TxXferSize = Size;//表示要发送的个数
huart->TxXferCount = Size;//还要发送的个数=总个数-已经发送
while (huart->TxXferCount > 0U)
{
huart->TxXferCount--;
if (huart->Init.WordLength == UART_WORDLENGTH_9B)
{
//等待标志发生,查看是否超时
if (UART_WaitOnFlagUntilTimeout(huart, UART_FLAG_TXE, RESET, tickstart, Timeout) != HAL_OK)
{
return HAL_TIMEOUT;//超时
}
tmp = (uint16_t *) pData;
huart->Instance->DR = (*tmp & (uint16_t)0x01FF);
if (huart->Init.Parity == UART_PARITY_NONE)
{
pData += 2U;
}
else
{
pData += 1U;
}
}
else
{
if (UART_WaitOnFlagUntilTimeout(huart, UART_FLAG_TXE, RESET, tickstart, Timeout) != HAL_OK)
{
return HAL_TIMEOUT;
}
//真正干活的代码
huart->Instance->DR = (*pData++ & (uint8_t)0xFF);
}
}
//查看发送是否已经完成【阻塞等待串口发完】
if (UART_WaitOnFlagUntilTimeout(huart, UART_FLAG_TC, RESET, tickstart, Timeout) != HAL_OK)
{
return HAL_TIMEOUT;
}
/* At end of Tx process, restore huart->gState to Ready */
//将状态设置回去
huart->gState = HAL_UART_STATE_READY;
/* Process Unlocked */
//解锁
__HAL_UNLOCK(huart);
return HAL_OK;
}
else
{
return HAL_BUSY;
}
}
1.2 阻塞式接收:HAL_UART_Receive(不常用)
HAL_StatusTypeDef HAL_UART_Receive(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout)
{
uint16_t *tmp;
uint32_t tickstart = 0U;
/* Check that a Rx process is not already ongoing */
if (huart->RxState == HAL_UART_STATE_READY)
{
if ((pData == NULL) || (Size == 0U))
{
return HAL_ERROR;
}
/* Process Locked */
__HAL_LOCK(huart);
huart->ErrorCode = HAL_UART_ERROR_NONE;
huart->RxState = HAL_UART_STATE_BUSY_RX;
/* Init tickstart for timeout managment */
tickstart = HAL_GetTick();
huart->RxXferSize = Size;
huart->RxXferCount = Size;
/* Check the remain data to be received */
while (huart->RxXferCount > 0U)
{
huart->RxXferCount--;
if (huart->Init.WordLength == UART_WORDLENGTH_9B)
{
//检查UART_FLAG_RXNE是否为空,如果为非空,表示接收到数据
if (UART_WaitOnFlagUntilTimeout(huart, UART_FLAG_RXNE, RESET, tickstart, Timeout) != HAL_OK)
{
return HAL_TIMEOUT;
}
tmp = (uint16_t *) pData;
if (huart->Init.Parity == UART_PARITY_NONE)
{
*tmp = (uint16_t)(huart->Instance->DR & (uint16_t)0x01FF);
pData += 2U;
}
else
{
*tmp = (uint16_t)(huart->Instance->DR & (uint16_t)0x00FF);
pData += 1U;
}
}
else
{
if (UART_WaitOnFlagUntilTimeout(huart, UART_FLAG_RXNE, RESET, tickstart, Timeout) != HAL_OK)
{
return HAL_TIMEOUT;
}
if (huart->Init.Parity == UART_PARITY_NONE)
{
*pData++ = (uint8_t)(huart->Instance->DR & (uint8_t)0x00FF);
}
else
{
*pData++ = (uint8_t)(huart->Instance->DR & (uint8_t)0x007F);
}
}
}
/* At end of Rx process, restore huart->RxState to Ready */
huart->RxState = HAL_UART_STATE_READY;
/* Process Unlocked */
__HAL_UNLOCK(huart);
return HAL_OK;
}
3.延时时间的设置
如果我们设置的延时时间太长,则我们可能会在延时的时候错过一些字符的发送和接收。
4.总结
int main(void)
{
//定义一个要进行发送的数据
uint8_t sbuf[8]="stm32";
//定义一个要进行接收的数据
uint8_t rbuf[20]="";
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_USART1_UART_Init();
while (1)
{
//第一个参数要传入的是地址
//size:要发送的大小
//0x0000ffff:超时时间
// HAL_UART_Transmit(&huart1,sbuf,5,0x0000ffff);
// HAL_Delay(1000);
//阻塞式的接收:接收一个后马上接着下一个接收
//此方法最好一次发送一个bit
HAL_UART_Receive(&huart1,rbuf,1,0x0000ffff);
//将接收到的数值发送出来
HAL_UART_Transmit(&huart1,rbuf,1,0x0000ffff);
//这个延时时间不能太久,要不然可能会在延时的时候接收了一个字符,从而错过这个
//HAL_Delay(100);
}
}
阻塞式的发送实际用的很多,因为编程简单。缺陷是浪费高速CPU的部分性能,没有追求到串口发送和整个系统性能的最高。
2、非阻塞式(中断)串口发送
1.HAL_UART_Transmit_IT
这个函数没有超时时间,因为我们不用等。
注意点::要打开中断
HAL_StatusTypeDef HAL_UART_Transmit_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)
{
/* Check that a Tx process is not already ongoing */
if (huart->gState == HAL_UART_STATE_READY)
{
if ((pData == NULL) || (Size == 0U))
{
return HAL_ERROR;
}
/* Process Locked */
__HAL_LOCK(huart);
//因为我们使用中断方式,则需要在串口内部定义一个buf,使得buf指向data的地址
huart->pTxBuffPtr = pData;
huart->TxXferSize = Size;
huart->TxXferCount = Size;
huart->ErrorCode = HAL_UART_ERROR_NONE;
huart->gState = HAL_UART_STATE_BUSY_TX;
/* Process Unlocked */
__HAL_UNLOCK(huart);
/* Enable the UART Transmit data register empty Interrupt */
//使能UART发送数据寄存器空中断
__HAL_UART_ENABLE_IT(huart, UART_IT_TXE);
return HAL_OK;
}
else
{
return HAL_BUSY;
}
}
2.注意点
我们使用这个中断式的应该打开Usart的中断
int main(void)
{
//定义一个要进行发送的数据
uint8_t sbuf[8]="stm32";
//定义一个要进行接收的数据
uint8_t rbuf[20]="";
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_USART1_UART_Init();
while (1)
{
/**
中断式发送和接收
*/
HAL_UART_Transmit_IT(&huart1,sbuf,5);
HAL_Delay(100);
}
}
3.printf重定向的实现
printf的实现其实就是重载fputc函数
要实现printf就要重写fputc
fputc–>printf
在使用USART(通用异步收发传输)时,
fputc
函数通常用于将一个字符发送到串口。fputc
是标准C库函数,它用于将一个字符写入指定的输出流(通常是标准输出流stdout
)。在使用USART进行串口通信时,
fputc
的使用场景可能是在使用标准C库的printf
函数时。printf
是一个格式化输出函数,通过重定向标准输出流,可以将格式化的数据发送到USART,从而实现通过串口发送数据的功能。
1.原始代码
#ifdef __GNUC__
#define PUTCHAR_PROTOTYPE int __io_putchar(int ch)
#else
#define PUTCHAR_PROTOTYPE int fputc(int ch, FILE *f)
#endif
/**
* @brief Retargets the C library printf function to the USART.
* @param None
* @retval None
*/
PUTCHAR_PROTOTYPE
{
/* Place your implementation of fputc here */
/* e.g. write a character to the EVAL_COM1 and Loop until the end of transmission */
HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xFFFF);
return ch;
}
2.注意点1:
一定一定要勾选User MicroLIB
3.注意点2:
我们使用到了fputc,其中FILE中是定义在<stdio.h>中的宏定义
定义在usart.h中
int main(void)
{
/* USER CODE BEGIN 1 */
//定义一个要进行发送的数据
uint8_t sbuf[8]="stm32";
//定义一个要进行接收的数据
uint8_t rbuf[20]="";
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_USART1_UART_Init();
while (1)
{
float a=4.532;
printf("a=%f\n",a);
printf("test\r");
}
}
4.串口接收编程实战
1、阻塞式串口接收
CPU等着过来。所以当CPU临时去处理其他事情时,可能会错过一些串口的输出。所以我们基本上不用。如果真的要使用,则要协调好延时和串口的接收和发送。
2、中断式串口接收
使用中断的方式向串口发送数据并且输出
1.先开启uart中断
2.写中断处理函数:HAL_UART_RxCpltCallback
在main函数中
//rc用来暂存和处理串口接收到的字节内容的
uint8_t receive_char;
//这个就是HAL库对接的中断处理程序
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
//判断是否为usart1
if(huart->Instance == USART1)
{
//这里就是真正的中断处理代码
//我们这里的处理就是接收到一个字节后原封不动的发回去
HAL_UART_Transmit(&huart1, &receive_char, 1,0xFF);//发送字符
//等待本次接收完毕,同时开启下一次接收【更新receive_char】
//HAL_UART_Receive_IT:开启中断处理流程
while(HAL_UART_Receive_IT(&huart1, &receive_char, 1) != HAL_OK);
}
}
3.注册中断
由上面分析可以知道,我们在进入中断处理函数中,是将接收到的字符发送出去,所以我们需要在外部先接收到字符【触发中断】,才可以发送。所以我们需要在其他模块初始化的位置先进入一次中断,接收串口发送的数据。
3.测试代码
此代码:在我们不向串口发送字符时,每3s发送一次a,当我们向串口发送数据时,会马上在串口输出。
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_USART1_UART_Init();
//从此处进入中断处理程序
HAL_UART_Receive_IT(&huart1, &receive_char, 1);
while (1)
{
float a=4.532;
printf("a=%f\n",a);
HAL_Delay(3000);
}
}
//这个就是HAL库对接的中断处理程序
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
//判断是否为usart1
if(huart->Instance == USART1)
{
//这里就是真正的中断处理代码
//我们这里的处理就是接收到一个字节后原封不动的发回去
HAL_UART_Transmit(&huart1, &receive_char, 1,0xFF);//发送字符
//等待本次接收完毕,同时开启下一次接收【更新receive_char】
//HAL_UART_Receive_IT:开启中断处理流程
while(HAL_UART_Receive_IT(&huart1, &receive_char, 1) != HAL_OK);
}
}
4.函数调用梳理
3.处理串口中断的流程
在初始化部分已经将中断连接起来,到时候产生中断则直接进入【HAL_UART_RxCpltCallback】这个函数
5.案例1:基于串口的命令shell实现
命令shell:发送一个命令返回一个回应
1.协议自定义
(1)指令集:指令1:add 指令2:sub
(2)指令结束符:';'【定义普通指令中不可能出现的】
(3)指令中遇到回车和空格、Tab等特殊字符怎么办
2.实现思路分析
1.定义一个缓冲区,存储接到的数据
2.定义一个索引值
值存储再buf中的第几个字节
3.代码编写
/* USER CODE BEGIN 4 */
//这个就是HAL库对接的中断处理程序
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
//判断是否为usart1
if(huart->Instance == USART1)
{
//将receive_char的数值存放再buf中
rev_buf[tindex]=receive_char;//暂存
tindex++;//指向下一个
while(HAL_UART_Receive_IT(&huart1, &receive_char, 1) != HAL_OK);
}
}
4.main函数编写
1)判断指令
2)buf和index要记得清空
6.串口实验
1.串口接收数据过程
2.串口发送数据过程
10.接收中断 && 中断回调
参考博客:
https://www.cnblogs.com/UnfriendlyARM/p/10321838.html
接收中断:HAL_UART_Receive_IT
中断回调:HAL_UART_RxCpltCallback
HAL_UART_Receive_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)”。该函数的作用是用户自定义一个缓冲区(即参数pData),接受一定数量(由参数Size决定)的字符存入缓冲区中。同时,参数Size还决定着进入回调函数的频率,即每接收Size个字符,就进入一次回调函数。需要注意的是,Size只决定进入回调函数的频率,而不能影响进入接收中断的频率,无论Size是多少,每接收完成一个字符都会进入一次接收中断。