Clion
STM32CubeMx开发环境
UART串口协议
stm32中断概念
实验程序已经发布到百度网盘,本文末有链接可以自取
串口协议查看这篇博客USART串口协议
stm32中断概念STM32中断应用概括
工具Clion和STM32CubeMx
通用同步
异步收发器 (Universal Synchronous Asynchronous Receiver and Transmitter) 是一个串行通信设备,可以灵活地与外部设备进行全双工数据交换。有别于 USART还有一个UART(Universal
Asynchronous Receiver and Transmitter) ,它是在 USART 基础上裁剪掉了同步通信功能,只有异步通信
。简单区分同步和异步就是看通信时需不需要对外提供时钟输出,我们平时用的串口通信基本都是 UART (异步通信)。
串行通信一般是以帧
格式传输数据,即是一帧一帧的传输,每帧包含有起始信号、数据信息、停止信息,可能还有校验信息
。 USART 就是对这些传输参数有具体规定,当然也不是只有唯一一个参数值,很多参数值都可以自定义设置,只是增强它的兼容性。
USART 满足外部设备对工业标准 NRZ 异步串行数据格式的要求,并且使用了小数波特率发生
器,可以提供多种波特率,使得它的应用更加广泛。 USART 支持同步单向通信和半双工单线通信;还支持局域互连网络 LIN、智能卡(SmartCard) 协议与 lrDA(红外线数据协会) SIR ENDEC 规范。USART 支持使用 DMA,可实现高速数据通信。
USART 在 STM32 应用最多莫过于“打印”程序信息,一般在硬件设计时都会预留一个 USART通信接口连接电脑,用于在调试程序是可以把一些调试信息“打印”在电脑端的串口调试助手工具上,从而了解程序运行是否正确、如果出错哪具体哪里出错等等
USART 的功能框图包含了 USART 最核心内容,掌握了功能框图,对 USART 就有一个整体的把握,在编程时就思路就非常清晰。 USART 功能框图见图 USART 功能框图 。
功能引脚
STM32F103C8T6 系统控制器有三个 USART,其中 USART1 和时钟来源于 APB2 总线时钟,其最大频率为 72MHz,其他两个的时钟来源于 APB1 总线时钟,其最大频率为 36MHz。
USART 数据寄存器 (USART_DR) 只有低 9 位有效,并且第 9 位数据是否有效要取决于 USART控制寄存器 1(USART_CR1) 的 M 位设置,当 M 位为 0 时表示 8 位数据字长,当 M 位为 1 表示 9位数据字长,我们一般使用 8 位数据字长。
USART_DR 包含了已发送的数据或者接收到的数据。 USART_DR 实际是包含了两个寄存器,一个专门用于发送的可写 TDR,一个专门用于接收的可读 RDR。当进行发送操作时,往 USART_DR写入数据会自动存储在 TDR 内;当进行读取操作时,向 USART_DR 读取数据会自动提取 RDR
数据。
TDR 和 RDR 都是介于系统总线和移位寄存器之间。串行通信是一个位一个位传输的,发送时把TDR 内容转移到发送移位寄存器,然后把移位寄存器数据每一位发送出去,接收时把接收到的每一位顺序保存在接收移位寄存器内然后才转移到 RDR。
USART 支持 DMA 传输,可以实现高速数据传输,具体 DMA 使用将在 DMA 章节讲解。
USART 有专门控制发送的发送器、控制接收的接收器,还有唤醒单元、中断控制等等。使用USART 之前需要向USART_CR1 寄存器的 UE 位置 1 使能 USART, UE 位用来开启供给给串口的时钟。
发送或者接收数据字长可选 8 位或 9 位,由 USART_CR1的 M 位控制。
当 USART_CR1 寄存器的发送使能位 TE 置 1 时,启动数据发送,发送移位寄存器的数据会在 TX引脚输出,低位在前,高位在后。如果是同步模式 SCLK 也输出时钟信号。
一个字符帧发送需要三个部分:起始位
+ 数据帧
+ 停止位
。起始位是一个位周期的低电平,位周期就是每一位占用的时间;数据帧就是我们要发送的 8 位或 9 位数据,数据是从最低位开始传输的;停止位是一定时间周期的高电平。
停止位时间长短是可以通过 USART 控制寄存器 2(USART_CR2) 的 STOP[1:0] 位控制,可选 0.5个、 1 个、 1.5 个和 2 个停止位。默认使用 1 个停止位。 2 个停止位适用于正常 USART 模式、单线模式和调制解调器模式。 0.5 个和 1.5 个停止位用于智能卡模式。
当选择 8 位字长,使用 1 个停止位时,具体发送字符时序图见图字符发送时序图 。
当发送使能位 TE 置 1 之后,发送器开始会先发送一个空闲帧 (一个数据帧长度的高电平),接下来就可以往 USART_DR 寄存器写入要发送的数据。在写入最后一个数据后,需要等待 USART 状态寄存器 (USART_SR) 的 TC 位为 1,表示数据传输完成,如果 USART_CR1 寄存器的 TCIE 位置为1,将产生中断。
在发送数据时,编程的时候有几个比较重要的标志位我们来总结下。
如果将 USART_CR1 寄存器的 RE 位置 1,使能 USART 接收,使得接收器在 RX 线开始搜索起始位。在确定到起始位后就根据 RX 线电平状态把数据存放在接收移位寄存器内。接收完成后就把接收移位寄存器数据移到 RDR 内,并把 USART_SR 寄存器的 RXNE 位 置1,同时如果USART_CR2 寄存器的 RXNEIE 置 1 的话可以产生中断。
在接收数据时,编程的时候有几个比较重要的标志位我们来总结下。
名称 | 描述 |
---|---|
RE | 接收使能 |
RXNE | 读数据寄存器非空 |
RXNEIE | 接收完成中断使能 |
波特率
指数据信号对载波的调制速率,它用单位时间内载波调制状态改变次数来表示,单位为波特。比特率指单位时间内传输的比特数,单位bit/s(bps)。对于 USART 波特率与比特率相等,以后不区分这两个概念。波特率越大,传输速率越快。
USART 的发送器和接收器使用相同的波特率。计算公式如下:
其中, fPLCK 为 USART 时钟, USARTDIV 是一个存放在波特率寄存器 (USART_BRR) 的一个无符号定点数。其中 DIV_Mantissa[11:0] 位定义 USARTDIV 的整数部分,DIV_Fraction[3:0] 位定义USARTDIV 的小数部分。
例如: DIV_Mantissa=24(0x18), DIV_Fraction=10(0x0A),此时 USART_BRR 值为 0x18A;那么 USARTDIV 的小数位 10/16=0.625;整数位 24,最终 USARTDIV 的值为 24.625。如果知道 USARTDIV 值为 27.68,那么 DIV_Fraction=16*0.68=10.88,最接近的正整数为 11,所以DIV_Fraction[3:0] 为 0xB; DIV_Mantissa= 整数 (27.68)=27,即为 0x1B。波特率的常用值有 2400、 9600、 19200、 115200。下面以实例讲解如何设定寄存器值得到波特率的值。
我们知道 USART1 使用 APB2 总线时钟,最高可达 72MHz,其他 USART 的最高频率为 36MHz。我们选取 USART1 作为实例讲解,即fPLCK=72MHz。为得到 115200bps 的波特率,此时:
115200 =72000000/(16 ∗ USARTDIV)
解得 USARTDIV=39.0625,可算得 DIV_Fraction=0.0625*16=1=0x01, DIV_Mantissa=39=0x27,即应该设置 USART_BRR 的值为 0x271。
STM32F103 系列控制器 USART 支持奇偶校验。当使用校验位时,串口传输的长度将是 8 位的数
据帧加上 1 位的校验位总共 9 位,此时 USART_CR1 寄存器的 M 位需要设置为 1,即 9 数据位。将 USART_CR1 寄存器的 PCE 位置 1 就可以启动奇偶校验控制,奇偶校验由硬件自动完成。启动了奇偶校验控制之后,在发送数据帧时会自动添加校验位,接收数据时自动验证校验位。接收数据时如果出现奇偶校验位验证失败,会见 USART_SR 寄存器的 PE 位置 1,并可以产生奇偶校验中断。
使能了奇偶校验控制后,每个字符帧的格式将变成:起始位 + 数据帧 + 校验位 + 停止位。
USART 有多个中断请求事件,具体见表 USART 中断请求 。
USART 只需两根信号线即可完成双向通信,对硬件要求低,使得很多模块都预留 USART 接口来实现与其他模块或者控制器进行数据传输,比如 GSM 模块, WIFI 模块、蓝牙模块等,以后都会在实验中提及,在硬件设计时,注意还需要一根“共地线”。
我们经常使用 USART 来实现控制器与电脑之间的数据传输。这使得我们调试程序非常方便,比如我们可以把一些变量的值、函数的返回值、寄存器标志位等等通过 USART 发送到串口调试助手,这样我们可以非常清楚程序的运行状态,当我们正式发布程序时再把这些调试信息去除即可。
我们不仅仅可以将数据发送到串口调试助手,我们还可以在串口调试助手发送数据给控制器,控制器程序根据接收到的数据进行下一步工作。
为利用 USART 实现开发板与电脑通信,需要用到一个 USB 转 USART 的 IC,我们选择CH340G
芯片来实现这个功能, CH340G 是一个 USB 总线的转接芯片,实现 USB 转 USART、 USB 转 lrDA红外或者 USB 转打印机接口,我们使用其 USB 转 USART 功能。
连接引脚
CH340的TXD-----USART1的RX引脚相连(c8t6 的PA10)
CH340的RXD-----USART1的TX引脚相连(c8t6 的PA9)
新建项目
找到自己对应的芯片型号
出现芯片型号选择 一般我们直接搜索自己芯片的型号即可
表示警告,对应配置出现问题 点击该选项即可外设配置界面查看
因为我们这里用LED,所以还要配置GPIO引脚
设置高速外部时钟HSE 选择外部时钟源
ST-Link 就是 Serial Wire 调试模式,一定要设置!!!
如果不配置 Serial Wire 模式,程序一旦通过 ST-Link 烧录到芯片中,芯片就再也不能被ST-Link 识别了。
ST-Link V2烧录问题(已解决)
外部晶振为8MHz
32的时钟树框图 《【STM32】系统时钟RCC详解(超详细,超全面)》
设置完MCU的各个配置之后,第三个就是工程文件的设置了
选择自己的配置文件
我这里是自定义配置文件,自己新建一个cfg文件:st_my_F1_stlink-v2.cfg
source [find interface/stlink-v2.cfg]
transport select hla_swd
source [find target/stm32f1x.cfg]
(7条消息) 基于Clion IDE + STM32CubeMX搭建STM32开发环境(详细介绍搭建过程)_Ch_champion的博客-CSDN博客_clion搭建stm32开发环境
UART结构体定义
UART_HandleTypeDef huart1;
UART的名称定义,这个结构体中存放了UART所有用到的功能,后面的别名就是我们所用的uart串口的别名,默认为huart1
可以自行修改
串口发送数据:
HAL_UART_Transmit(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout)
功能:串口发送指定长度的数据。如果超时没发送完成,则不再发送,返回超时标志(HAL_TIMEOUT)。
参数:
举例: HAL_UART_Transmit(&huart1, (uint8_t *)ZZX, 3, 0xffff); //串口发送三个字节数据,最大传输时间0xffff
中断接收数据:
HAL_UART_Receive_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)
功能:串口中断接收,以中断方式接收指定长度数据。
大致过程是,设置数据存放位置,接收数据长度,然后使能串口接收中断。接收到数据时,会触发串口中断。
再然后,串口中断函数处理,直到接收到指定长度数据,而后关闭中断,进入中断接收回调函数,不再触发接收中断。(只触发一次中断)
参数:
举例: HAL_UART_Receive_IT(&huart1,(uint8_t *)&value,1); //中断接收一个字符,存储到value中
串口接收中断回调函数:
HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart);
功能:HAL库的中断进行完之后,并不会直接退出,而是会进入中断回调函数中,用户可以在其中设置代码,
串口中断接收完成之后,会进入该函数,该函数为空函数,用户需自行修改,
参数:
举例: HAL_UART_RxCpltCallback(&huart1){ //用户设定的代码 }
串口中断处理函数
HAL_UART_IRQHandler(UART_HandleTypeDef *huart);
功能:对接收到的数据进行判断和处理 判断是发送中断还是接收中断,然后进行数据的发送和接收,在中断服务函数中使用
如果接收数据,则会进行接收中断处理函数
/* UART in mode Receiver ---------------------------------------------------*/
if((tmp_flag != RESET) && (tmp_it_source != RESET))
{
UART_Receive_IT(huart);
}
如果发送数据,则会进行发送中断处理函数
/* UART in mode Transmitter ------------------------------------------------*/
if (((isrflags & USART_SR_TXE) != RESET) && ((cr1its & USART_CR1_TXEIE) != RESET))
{
UART_Transmit_IT(huart);
return;
}
HAL_UART_GetState(); 判断UART的接收是否结束,或者发送数据是否忙碌
举例:
while(HAL_UART_GetState(&huart4) == HAL_UART_STATE_BUSY_TX) //检测UART发送结束
#ifndef _RETARGET_H__
#define _RETARGET_H__
#include "stm32f1xx_hal.h"
#include
#include
void RetargetInit(UART_HandleTypeDef *huart);
int _isatty(int fd);
int _write(int fd, char *ptr, int len);
int _close(int fd);
int _lseek(int fd, int ptr, int dir);
int _read(int fd, char *ptr, int len);
int _fstat(int fd, struct stat *st);
#endif //#ifndef _RETARGET_H__
#include <_ansi.h>
#include <_syslist.h>
#include
#include
#include
#include
#include
#if !defined(OS_USE_SEMIHOSTING)
#define STDIN_FILENO 0
#define STDOUT_FILENO 1
#define STDERR_FILENO 2
UART_HandleTypeDef *gHuart;
void RetargetInit(UART_HandleTypeDef *huart) {
gHuart = huart;
/* Disable I/O buffering for STDOUT stream, so that
* chars are sent out as soon as they are printed. */
setvbuf(stdout, NULL, _IONBF, 0);
}
int _isatty(int fd) {
if (fd >= STDIN_FILENO && fd <= STDERR_FILENO)
return 1;
errno = EBADF;
return 0;
}
int _write(int fd, char *ptr, int len) {
HAL_StatusTypeDef hstatus;
if (fd == STDOUT_FILENO || fd == STDERR_FILENO) {
hstatus = HAL_UART_Transmit(gHuart, (uint8_t *) ptr, len, HAL_MAX_DELAY);
if (hstatus == HAL_OK)
return len;
else
return EIO;
}
errno = EBADF;
return -1;
}
int _close(int fd) {
if (fd >= STDIN_FILENO && fd <= STDERR_FILENO)
return 0;
errno = EBADF;
return -1;
}
int _lseek(int fd, int ptr, int dir) {
(void) fd;
(void) ptr;
(void) dir;
errno = EBADF;
return -1;
}
int _read(int fd, char *ptr, int len) {
HAL_StatusTypeDef hstatus;
if (fd == STDIN_FILENO) {
hstatus = HAL_UART_Receive(gHuart, (uint8_t *) ptr, 1, HAL_MAX_DELAY);
if (hstatus == HAL_OK)
return 1;
else
return EIO;
}
errno = EBADF;
return -1;
}
int _fstat(int fd, struct stat *st) {
if (fd >= STDIN_FILENO && fd <= STDERR_FILENO) {
st->st_mode = S_IFCHR;
return 0;
}
errno = EBADF;
return 0;
}
#endif //#if !defined(OS_USE_SEMIHOSTING)
添加这两个文件到工程,更新CMake,编译之后会发现,有几个系统函数重复定义了,被重复定义的函数位于Src目录的syscalls.c文件中,我们把里面重复的几个函数删掉即可。
/**
******************************************************************************
* @file syscalls.c
* @author Auto-generated by STM32CubeIDE
* @brief STM32CubeIDE Minimal System calls file
*
* For more information about which c-functions
* need which of these lowlevel functions
* please consult the Newlib libc-manual
******************************************************************************
* @attention
*
* Copyright (c) 2021 STMicroelectronics.
* All rights reserved.
*
* This software is licensed under terms that can be found in the LICENSE file
* in the root directory of this software component.
* If no LICENSE file comes with this software, it is provided AS-IS.
*
******************************************************************************
*/
/* Includes */
#include
#include
#include
#include
#include
#include
#include
#include
/* Variables */
extern int __io_putchar(int ch) __attribute__((weak));
extern int __io_getchar(void) __attribute__((weak));
char *__env[1] = { 0 };
char **environ = __env;
/* Functions */
void initialise_monitor_handles()
{
}
int _getpid(void)
{
return 1;
}
int _kill(int pid, int sig)
{
errno = EINVAL;
return -1;
}
void _exit (int status)
{
_kill(status, -1);
while (1) {} /* Make sure we hang here */
}
int _open(char *path, int flags, ...)
{
/* Pretend like we always fail */
return -1;
}
int _wait(int *status)
{
errno = ECHILD;
return -1;
}
int _unlink(char *name)
{
errno = ENOENT;
return -1;
}
int _times(struct tms *buf)
{
return -1;
}
int _stat(char *file, struct stat *st)
{
st->st_mode = S_IFCHR;
return 0;
}
int _link(char *old, char *new)
{
errno = EMLINK;
return -1;
}
int _fork(void)
{
errno = EAGAIN;
return -1;
}
int _execve(char *name, char **argv, char **env)
{
errno = ENOMEM;
return -1;
}
在main函数的初始化代码中添加对头文件的引用并注册重定向的串口号:
#include "retarget.h"
RetargetInit(&huart1);
/* USER CODE BEGIN Includes */
#include "retarget.h"
/* USER CODE END Includes */
void SystemClock_Config(void);
int main(void) {
HAL_Init();
SystemClock_Config();
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_USART1_UART_Init();
/* USER CODE BEGIN 2 */
RetargetInit(&huart1);
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
printf("欢迎来到洛尘的CSDN");
while (1) {
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
因为中断接收函数只能触发一次接收中断,所以我们需要在中断回调函数中再调用一次中断接收函数
1、初始化串口
2、在main中第一次调用接收中断函数
3、进入接收中断,接收完数据 进入中断回调函数
4、修改HAL_UART_RxCpltCallback中断回调函数,处理接收的数据,
5、回调函数中要调用一次HAL_UART_Receive_IT函数,使得程序可以重新触发接收中断
函数流程图:
HAL_UART_Receive_IT(中断接收函数) -> USART2_IRQHandler(void)(中断服务函数) -> HAL_UART_IRQHandler(UART_HandleTypeDef *huart)(中断处理函数) -> UART_Receive_IT(UART_HandleTypeDef *huart) (接收函数) -> HAL_UART_RxCpltCallback(huart); (中断回调函数)
HAL_UART_RxCpltCallback函数就是用户要重写在main.c里的回调函数。
代码实现:
在main.c中添加下列定义:
#include
#define RXBUFFERSIZE 256 //最大接收字节数
char RxBuffer[RXBUFFERSIZE]; //接收数据
uint8_t aRxBuffer; //接收中断缓冲
uint8_t Uart1_Rx_Cnt = 0; //接收缓冲计数
在main()主函数中,调用一次接收中断函数
/* USER CODE BEGIN 2 */
HAL_UART_Receive_IT(&huart1, (uint8_t *)&aRxBuffer, 1);
/* USER CODE END 2 */
在main.c下方添加中断回调函数
/* USER CODE BEGIN 4 */
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
/* Prevent unused argument(s) compilation warning */
UNUSED(huart);
/* NOTE: This function Should not be modified, when the callback is needed,
the HAL_UART_TxCpltCallback could be implemented in the user file
*/
if(Uart1_Rx_Cnt >= 255) //溢出判断
{
Uart1_Rx_Cnt = 0;
memset(RxBuffer,0x00,sizeof(RxBuffer));
HAL_UART_Transmit(&huart1, (uint8_t *)"数据溢出", 10,0xFFFF);
}
else
{
RxBuffer[Uart1_Rx_Cnt++] = aRxBuffer; //接收数据转存
if((RxBuffer[Uart1_Rx_Cnt-1] == 0x0A)&&(RxBuffer[Uart1_Rx_Cnt-2] == 0x0D)) //判断结束位
{
HAL_UART_Transmit(&huart1, (uint8_t *)&RxBuffer, Uart1_Rx_Cnt,0xFFFF); //将收到的信息发送出去
while(HAL_UART_GetState(&huart1) == HAL_UART_STATE_BUSY_TX);//检测UART发送结束
Uart1_Rx_Cnt = 0;
memset(RxBuffer,0x00,sizeof(RxBuffer)); //清空数组
}
}
HAL_UART_Receive_IT(&huart1, (uint8_t *)&aRxBuffer, 1); //再开启接收中断
}
/* USER CODE END 4 */
发送数据被正常返回
我们只用将usrt1的HAL_UART_RxCpltCallback函数改一下就行。
/* USER CODE BEGIN 4 */
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
/* Prevent unused argument(s) compilation warning */
UNUSED(huart);
/* NOTE: This function Should not be modified, when the callback is needed,
the HAL_UART_TxCpltCallback could be implemented in the user file
*/
if (Uart1_Rx_Cnt >= 255) //溢出判断
{
Uart1_Rx_Cnt = 0;
memset(RxBuffer, 0x00, sizeof(RxBuffer));
HAL_UART_Transmit(&huart1, (uint8_t *) "数据溢出", 10, 0xFFFF);
} else {
RxBuffer[Uart1_Rx_Cnt++] = aRxBuffer; //接收数据转存
if ((RxBuffer[Uart1_Rx_Cnt - 1] == 0x0A) && (RxBuffer[Uart1_Rx_Cnt - 2] == 0x0D)) //判断结束位
{
HAL_UART_Transmit(&huart1, (uint8_t *) &RxBuffer, Uart1_Rx_Cnt, 0xFFFF); //将收到的信息发送出去
while (HAL_UART_GetState(&huart1) == HAL_UART_STATE_BUSY_TX);//检测UART发送结束
if (strstr(RxBuffer, "LED=0")) {
HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, 0); //PC口13引脚输出,高电平
printf("LED亮\r\n");
} else if (strstr(RxBuffer, "LED=1")) {
HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, 1); //PC口13引脚输出,低电平
printf("LED灭\r\n");
}
Uart1_Rx_Cnt = 0;
memset(RxBuffer, 0x00, sizeof(RxBuffer)); //清空数组
}
}
HAL_UART_Receive_IT(&huart1, (uint8_t *) &aRxBuffer, 1); //再开启接收中断
}
查看USART串口printf重定向中文乱码这篇博客
链接:https://pan.baidu.com/s/1BLdJxgdwOHnf209x4AmIKg 提取码:0000