串口基础参考:嵌入式常见接口协议总结_路溪非溪的博客-CSDN博客
串口内容补充
在STM32中的参考手册中,只讲了USART,并没有提及UART,一开始很疑惑。
难道STM32没有UART?
后面看了数据手册,里面讲了有3个USART和2个UART。
这里讲一下这两个的区别。
UART是通用异步收发器,而USART是通用同步异步收发器。
一般而言,单片机中:
- 名称为UART的接口一般只能用于异步串行通讯。
- 名称为USART的接口既可以用于同步串行通讯,也能用于异步串行通讯。
事实上当我们使用USART在异步通信的时候,它与UART没有什么差别。
可是用在同步通信的时候,差别就非常明显了:大家都知道同步通信需要时钟来触发数据传输,也就是说USART相对UART的差别之中的一个就是能提供主动时钟。
如STM32的USART能够提供时钟支持ISO7816的智能卡接口。在实际使用中,基本不会用到USART的同步功能,又因为USART异步功能和UART一样,所以,基本上可以看做STM32有5个UART。
除了开头链接中讲的基本概念外,以下再补充一些关于串口的知识:
通信原理
UART用一条传输线将数据一位位地顺序传送,以字符为传输单位。通信中两个字符间的时间间隔多少是不固定的,然而在同一个字符中的两个相邻位间的时间间隔是固定的。
一个字符的信息由起始位、数据位、奇偶校验位和停止位组成。
首先明确,高电平为空闲位;
- 起始位: 先发出一个逻辑0信号, 表示传输字符的开始,因为串口的起始位都是统一规定的,所以通常不必特意设置。
- 数据位: 在STM32中,支持8位(包含奇偶校验位)或9位数据位(包含奇偶校验位)
- 校验位: 数据位加上这一位后, 使得1的位数应为偶数(偶校验)或奇数(奇校验)
- 停止位: 它是一个字符数据的结束标志. 可以是1位、1.5位、2位的高电平。
- 空闲位: 处于逻辑1状态, 表示当前线路上没有数据传送。
根据以上可知,每传递一个字符,在不需要奇偶校验的情况下,需要传送10bit。
常用的波特率有4800、9600、115200,最常用的就是9600。
115200虽然速度快,但是不太稳定,传输距离近。
查看参考手册
32F103xE有5个USART接口。
3个USART,2个UART.
更多功能描述和寄存器操作细节自行查阅参考手册。
老实讲,STM32的参考手册讲的USART比较多,看不太懂,在此记录UART相关的重点内容。
◆PCLK1用于USART2、3、4、5,PCLK2用于USART1
◆全双工的,异步通信。
◆USART利用分数波特率发生器提供宽范围的波特率选择。不要在通信进行中改变波特率寄存器的数值。
◆使用多缓冲器配置的DMA方式,可以实现高速数据通信。
◆总线在发送或接收前应处于空闲状态
◆一个起始位
◆可编程数据字长度(8位或9位)
◆校验控制
─ 发送校验位
─ 对接收数据进行校验
◆可配置的停止位-支持1或2个停止位,由此表明数据帧的结束
◆空闲帧包括了停止位。
◆单独的发送器和接收器使能位
◆检测标志
─ 接收缓冲器满
─ 发送缓冲器空
─ 传输结束标志
◆四个错误检测标志
─ 溢出错误
─ 噪音错误
─ 帧错误
─ 校验错误
◆RX:接收数据串行输。通过过采样技术来区别数据和噪音,从而恢复数据。
◆TX:发送数据输出。当发送器被禁止时,输出引脚恢复到它的I/O端口配置。当发送器被激活, 并且不发送数据时,TX引脚处于高电平。
查看原理图
查看电路设计,如果有使用到相关芯片,再查看相关芯片的数据手册。
这里的CH340是一个USB和TTL的转换芯片。
接到了单片机的USART1接口:
因为需要向PC端打印调试信息,所以,当前只关注发送引脚,即PA9
初始化
打开之前的工程,打开MX文件,接着配置串口。
1、起始位
2、数据位
3、校验位
4、停止位
5、波特率
具体过程如下:
选中USAT1
模式这里选择异步,就相当于是UART
硬件流控不需要。
接着进行基本参数的设置。
注意
字长包含了奇偶校验位
如果不设置奇偶校验位,字长就选8位;
如果设置奇偶校验位,字长就选择9位;
总之,保证数据位是8位
停止位1位即可
采样率默认即可
配置示例:
其他选项可暂时不管。
OK,重新生成代码。
打开新代码,可以看到,多了usart.c文件,以及对应的头文件。
文件里面同样是自动生成的该外设的初始化代码。
看一下其中的一个初始化函数:
void MX_USART1_UART_Init(void) { /* USER CODE BEGIN USART1_Init 0 */ /* USER CODE END USART1_Init 0 */ /* USER CODE BEGIN USART1_Init 1 */ /* USER CODE END USART1_Init 1 */ huart1.Instance = USART1; huart1.Init.BaudRate = 115200; huart1.Init.WordLength = UART_WORDLENGTH_8B; huart1.Init.StopBits = UART_STOPBITS_1; huart1.Init.Parity = UART_PARITY_NONE; huart1.Init.Mode = UART_MODE_TX; huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE; huart1.Init.OverSampling = UART_OVERSAMPLING_16; if (HAL_UART_Init(&huart1) != HAL_OK) { Error_Handler(); } /* USER CODE BEGIN USART1_Init 2 */ /* USER CODE END USART1_Init 2 */ }
第一行
huart1是一个结构体变量,通过调用Instance指针来设置USART1各寄存器的地址。通常,初始化第一步就是这样,定义好寄存器的各地址,方便后续操作。
第二行及往后,都是在设置串口,分别是波特率、数据位长度、停止位、奇偶校验、发送还是接收、流控、采样等。这里,和MX中设置的是一致的。
电路连接
将USB连接到单片机的串口电路,并在PC端安装CH340驱动。
打开SecureCRT,创建终端。
点击确定连接。
查看HAL串口库函数
前期工作都准备好了,现在开始写代码,通过串口输出信息。
那么,HAL中串口相关的函数有哪些呢?
查看相关驱动层文件stm32f1xx_hal_uart.h和stm32f1xx_hal_uart.c
有很多输出函数:
/* Exported functions --------------------------------------------------------*/ /** @addtogroup UART_Exported_Functions * @{ */ /** @addtogroup UART_Exported_Functions_Group1 Initialization and de-initialization functions * @{ */ /* Initialization/de-initialization functions **********************************/ HAL_StatusTypeDef HAL_UART_Init(UART_HandleTypeDef *huart); HAL_StatusTypeDef HAL_HalfDuplex_Init(UART_HandleTypeDef *huart); HAL_StatusTypeDef HAL_LIN_Init(UART_HandleTypeDef *huart, uint32_t BreakDetectLength); HAL_StatusTypeDef HAL_MultiProcessor_Init(UART_HandleTypeDef *huart, uint8_t Address, uint32_t WakeUpMethod); HAL_StatusTypeDef HAL_UART_DeInit(UART_HandleTypeDef *huart); void HAL_UART_MspInit(UART_HandleTypeDef *huart); void HAL_UART_MspDeInit(UART_HandleTypeDef *huart); /* Callbacks Register/UnRegister functions ***********************************/ #if (USE_HAL_UART_REGISTER_CALLBACKS == 1) HAL_StatusTypeDef HAL_UART_RegisterCallback(UART_HandleTypeDef *huart, HAL_UART_CallbackIDTypeDef CallbackID, pUART_CallbackTypeDef pCallback); HAL_StatusTypeDef HAL_UART_UnRegisterCallback(UART_HandleTypeDef *huart, HAL_UART_CallbackIDTypeDef CallbackID); HAL_StatusTypeDef HAL_UART_RegisterRxEventCallback(UART_HandleTypeDef *huart, pUART_RxEventCallbackTypeDef pCallback); HAL_StatusTypeDef HAL_UART_UnRegisterRxEventCallback(UART_HandleTypeDef *huart); #endif /* USE_HAL_UART_REGISTER_CALLBACKS */ /** * @} */ /** @addtogroup UART_Exported_Functions_Group2 IO operation functions * @{ */ /* IO operation functions *******************************************************/ HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout); HAL_StatusTypeDef HAL_UART_Receive(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout); HAL_StatusTypeDef HAL_UART_Transmit_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size); HAL_StatusTypeDef HAL_UART_Receive_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size); HAL_StatusTypeDef HAL_UART_Transmit_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size); HAL_StatusTypeDef HAL_UART_Receive_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size); HAL_StatusTypeDef HAL_UART_DMAPause(UART_HandleTypeDef *huart); HAL_StatusTypeDef HAL_UART_DMAResume(UART_HandleTypeDef *huart); HAL_StatusTypeDef HAL_UART_DMAStop(UART_HandleTypeDef *huart); HAL_StatusTypeDef HAL_UARTEx_ReceiveToIdle(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint16_t *RxLen, uint32_t Timeout); HAL_StatusTypeDef HAL_UARTEx_ReceiveToIdle_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size); HAL_StatusTypeDef HAL_UARTEx_ReceiveToIdle_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size); /* Transfer Abort functions */ HAL_StatusTypeDef HAL_UART_Abort(UART_HandleTypeDef *huart); HAL_StatusTypeDef HAL_UART_AbortTransmit(UART_HandleTypeDef *huart); HAL_StatusTypeDef HAL_UART_AbortReceive(UART_HandleTypeDef *huart); HAL_StatusTypeDef HAL_UART_Abort_IT(UART_HandleTypeDef *huart); HAL_StatusTypeDef HAL_UART_AbortTransmit_IT(UART_HandleTypeDef *huart); HAL_StatusTypeDef HAL_UART_AbortReceive_IT(UART_HandleTypeDef *huart); void HAL_UART_IRQHandler(UART_HandleTypeDef *huart); void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart); void HAL_UART_TxHalfCpltCallback(UART_HandleTypeDef *huart); void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart); void HAL_UART_RxHalfCpltCallback(UART_HandleTypeDef *huart); void HAL_UART_ErrorCallback(UART_HandleTypeDef *huart); void HAL_UART_AbortCpltCallback(UART_HandleTypeDef *huart); void HAL_UART_AbortTransmitCpltCallback(UART_HandleTypeDef *huart); void HAL_UART_AbortReceiveCpltCallback(UART_HandleTypeDef *huart); void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size); /** * @} */ /** @addtogroup UART_Exported_Functions_Group3 * @{ */ /* Peripheral Control functions ************************************************/ HAL_StatusTypeDef HAL_LIN_SendBreak(UART_HandleTypeDef *huart); HAL_StatusTypeDef HAL_MultiProcessor_EnterMuteMode(UART_HandleTypeDef *huart); HAL_StatusTypeDef HAL_MultiProcessor_ExitMuteMode(UART_HandleTypeDef *huart); HAL_StatusTypeDef HAL_HalfDuplex_EnableTransmitter(UART_HandleTypeDef *huart); HAL_StatusTypeDef HAL_HalfDuplex_EnableReceiver(UART_HandleTypeDef *huart); /** * @} */ /** @addtogroup UART_Exported_Functions_Group4 * @{ */ /* Peripheral State functions **************************************************/ HAL_UART_StateTypeDef HAL_UART_GetState(UART_HandleTypeDef *huart); uint32_t HAL_UART_GetError(UART_HandleTypeDef *huart);
当前,我们需要向PC端发送信息,所以目前重点关注函数是IO operation functions中的发送函数,发送结束后不用中断提醒,采用常规的轮询模式来发送即可。
HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout);
其返回值是一个状态:
typedef enum { HAL_OK = 0x00U, HAL_ERROR = 0x01U, HAL_BUSY = 0x02U, HAL_TIMEOUT = 0x03U } HAL_StatusTypeDef;
第一个参数是指定哪个串口,已经在初始化中定义过了,具体参数就是初始化的参数:
UART_HandleTypeDef huart1;
第二个参数是个char *,也就是要发送的字符/字符串;
第三个参数是要发送的内容的字节大小;
第四个参数是超时时间,也就是说,必须在规定的时间内发送完,因为开串口的时候用的是systick,所以是毫秒为单位(HAL库中只有少量的TimeOut是us级别,大部分都是ms级别;
具体实现,查看定义:
/** * @brief Sends an amount of data in blocking mode. * @note When UART parity is not enabled (PCE = 0), and Word Length is configured to 9 bits (M1-M0 = 01), * the sent data is handled as a set of u16. In this case, Size must indicate the number * of u16 provided through pData. * @param huart Pointer to a UART_HandleTypeDef structure that contains * the configuration information for the specified UART module. * @param pData Pointer to data buffer (u8 or u16 data elements). * @param Size Amount of data elements (u8 or u16) to be sent * @param Timeout Timeout duration * @retval HAL status */ HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout) { uint8_t *pdata8bits; uint16_t *pdata16bits; 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_LOCK(huart); huart->ErrorCode = HAL_UART_ERROR_NONE; huart->gState = HAL_UART_STATE_BUSY_TX; /* Init tickstart for timeout management */ tickstart = HAL_GetTick(); huart->TxXferSize = Size; huart->TxXferCount = Size; /* In case of 9bits/No Parity transfer, pData needs to be handled as a uint16_t pointer */ if ((huart->Init.WordLength == UART_WORDLENGTH_9B) && (huart->Init.Parity == UART_PARITY_NONE)) { pdata8bits = NULL; pdata16bits = (uint16_t *) pData; } else { pdata8bits = pData; pdata16bits = NULL; } /* Process Unlocked */ __HAL_UNLOCK(huart); while (huart->TxXferCount > 0U) { if (UART_WaitOnFlagUntilTimeout(huart, UART_FLAG_TXE, RESET, tickstart, Timeout) != HAL_OK) { return HAL_TIMEOUT; } if (pdata8bits == NULL) { huart->Instance->DR = (uint16_t)(*pdata16bits & 0x01FFU); pdata16bits++; } else { huart->Instance->DR = (uint8_t)(*pdata8bits & 0xFFU); pdata8bits++; } huart->TxXferCount--; } 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; return HAL_OK; } else { return HAL_BUSY; } }
代码实现
在myapplication.h中添加usart.h头文件。
本来是可以添加新文件来实现串口的,不过串口算是一个各个地方都会用到的函数,所以直接放到public文件中。
在public.h中的结构体中增加一个函数指针。
#ifndef _PUBLIC_H_ #define _PUBLIC_H_ #include "stdint.h" typedef struct { void (*printToScreen)(uint8_t * stringSize); } public_operator; extern public_operator publicOperator; #endif
实现对应的public.c文件
#include "myapplication.h" static void PrintToScreen(uint8_t *); public_operator publicOperator = { PrintToScreen }; static void PrintToScreen(uint8_t *string) { uint16_t stringSize= sizeof(string); uint32_t outtime = stringSize * 8 / 115200 * 1000 + 5; HAL_UART_Transmit(&huart1, string, stringSize, outtime); }
在设备初始化函数中调用
static void Peripheral_Set(void) { publicOperator.printToScreen((uint8_t *)"program now begin!"); }
以上代码编译烧录后,会发现可以输出到屏幕上,但是只能看到prog这四个字母,为啥?
这里又犯了一个指针传递的问题,使用sizeof时,一定要注意你算出来的是指针的大小还是指针指向的数据的大小。
当我将“program now begin!”传入发送函数时,我其实传入的是一个首字符的首地址
(uint8_t *)"program now begin!" //强转后是个地址
在发送函数中,使用sizeof计算出的,实际上是指针的大小,也就是4个字节。所以,字节和超时时间的计算都错了。这就是为什么只能打印出4个字节的原因。
这里,先将后两个参数写死吧。
static void PrintToScreen(uint8_t *string) { HAL_UART_Transmit(&huart1, string, 20, 5); }
成功输出:
代码优化
printf函数。
我们输出打印信息,通常会使用printf函数,这是标准输入输出头文件stdio.h中定义的函数。为了更加通用,我们能不能去重写printf,让printf的输出被重定位到我们的串口上呢?
为了实现这一目标,我们要先了解一下printf函数的源码。
跳转到printf的定义,只能到这里:
/* * is equivalent to fprintf, but does not support floating-point formats. * You can use instead of fprintf to improve code size. * Returns: as fprintf. */ #pragma __printf_args extern _ARMABI int printf(const char * __restrict /*format*/, ...) __attribute__((__nonnull__(1)));
再想跳转,跳不过去了。难道底层源码不对外开放?
看不到源码,只能去其他地方找资料了。将收集到的资料进行记录。
printf函数是将输入的内容以某种指定的格式输出到指定的端口,所以,在printf函数中,有两个最关键的函数:
一个是格式化函数vsprintf,主要功能是格式化打印信息,最终得到纯字符串格式的信息等待输出;
另外一个就是输出字符串的函数,要知道,在C中,是没有真正意义上的字符串的,所以,printf中输出字符串,是通过一个循环来一个一个输出字符来实现的。这个字符输出函数是fputc,该函数也是c的一个标准库函数,被printf调用,输出到指定的硬件上:
C 库函数 int fputc(int ch, FILE *stream) 把参数 char 指定的字符(一个无符号字符)写入到指定的流 stream 中,并把位置标识符往前移动。
网上一个课程里写了个大概实现,参考:
移植的时候,格式化不用管。
既然printf是通过调用fputc来输出的,那么,我们就重写fputc,让输出通过串口输出。
_weak
既然要重写fputc,那么,问题来了,我们根本就进不去fputc这个函数,而且,人家写的好好的库函数,肯定不会轻易让你修改,一旦修改,肯定很多地方都会出错。
怎么办呢?
虽然没有在网上找到相关资料。但是,听课程里说,原fputc函数是个弱函数,在该函数前面加了_weak关键字。
函数名称前面加上__weak 修饰符,我们一般称这个函数为“弱函数”。
加上了__weak 修饰符的函数,用户可以在用户文件中重新定义一个同名函数,最终编译器编译的时候,会选择用户定义的函数,如果用户没有重新定义这个函数,那么编译器就会执行__weak 声明的函数,并且编译器不会报错。所以我们可以在别的地方定义一个相同名字的函数,而不必也尽量不要修改之前的函数,。
可见,弱函数就是个“备胎”,先用着,有了正式的就自然而然地被替换掉了。
更多详情参考:嵌入式C进阶三 —— weak | 陌路之引
fputc重写
于是,我们就可以直接写个fputc函数,就能实现重写fputc的目的。
这样,也不用再封装结构体来实现串口输出了。
将之前public中串口相关的内容删除。然后重写fputc,fputc中指定输出到串口即可。
int fputc(int ch, FILE *stream) { HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 5); return ch; }
重写了fputc,也在外设初始化代码那里调用了printf
static void Peripheral_Set(void) { printf("2022-09-07"); }
但是很奇怪,不仅没有输出字符串,而且,连状态机的流水灯也没了。
问题在哪里?
仔细想了想,会不会是设置的问题?
记起来之前学习工程设置时,好像有时候需要勾选工程里的这个选项,表示要使用自带的标准库:
勾选之后,再下载,果然就可以了。
也是,printf本来就是自带的库函数,既然用到了,这里肯定要勾选。
至此,只重写了fputc函数,就实现了串口打印。
实现了printf的移植,可以使用其格式化方式,并且,输出到串口中查看输出。