STM32F103C8T6最小系统板(电控)学习指南

本文最后更新于:2025年1月20日 凌晨

学习stm32的基本开发笔记~

学习一下这块最小系统板

不错的讲解,点这里

Stm32F103C8T6的常用引脚

STM32F103C8T6引脚功能整理

  1. 通用I/O口
  • PA0-PA15: 16个通用I/O引脚,可用于输入/输出、外部中断、模拟输入等。

  • PB0-PB15: 16个通用I/O引脚,可用于输入/输出、外部中断、模拟输入等。

  • PC13-PC15: 3个通用I/O引脚,可用于输入/输出、外部中断等。

  • PD0-PD2: 3个通用I/O引脚,可用于输入/输出、外部中断等。

  • PE0-PE5: 6个通用I/O引脚,可用于输入/输出、外部中断等。

  • PF0-PF1: 2个通用I/O引脚,可用于输入/输出、外部中断等。

  1. 晶振引脚
  • 3456号引脚口,时钟晶振引脚口,
  • PC14-OSC32_IN,PC15-OSC32_OUT,32.768kHz的晶振
  • OSC_IN ,OSC_OUT,8MHz的晶振。主晶振。
  1. 下载端口
  • PA13:JTMS/SWDIO
  • PA14:JTCK/SWCLK
  1. 串口
  • PA9,PA10:USART1_TX,USART1_RX
  • PA2,PA3:USART2_TX,USART2_RX
  1. IIC
  • PB6,PB7:I2C1_SCL,I2C1_SDA
  • PB10,PB11:I2C2_SCL,I2C2_SDA
  1. SPI
  • PA4,SPI1 NSS;PA5,SPI1 SCK,PA6,SPI1 MISO;PA7,SPI1 MOSI
  • PA12,SPI2 NSS;PA13,SPI2 SCK,PA14,SPI2 MISO;PA15,SPI2 MOSI

面包板

简单的说,面包板是一种电子实验用品,表面是打孔的塑料,底部有金属条,电子元器件按照一定规则插上即可使用无需焊接。

面包板

面包板的讲解和使用方法

STM32CubeMX配置

不错的教程,点这里

基础配置

  1. 打开STM32CubeMX,单击ACCESS TO MCU SELECTOR
  2. 选择STM32F103C8T6芯片,创建project
  3. 进入配置界面后单击System Core(系统的核心) → SYS → Debug → Serial Wire
  4. 单击System Core(系统的核心) → RCC(配置晶振) → High speed Clock(HSE)(高速晶振)→ Crystal/Ceramic Resonator(外部晶振,8M)
  • 如果这里选Disable则无法使用外部高速晶振,这时PD0与PD1被用来做晶振的接口,如果不配置则可以把PD0、PD1当做普通IO口使用(新手建议还是进行配置)
  1. 单击Clock Configuration在这里输入72,按下回车 → OK,自动配置时钟频率为72Mhz

配置STM32外部时钟源

配置外部时钟源

需求配置(GPIO),以点灯为例

  1. 依次单击Pinout & Configuration → System Core → GPIO →右边的PC13(点灯) → GPIO_Output
  • 根据电路图,PC13应为输出高电平,即3.3V时,LED灯熄灭,0V低电平时LED灯,交替输出可以实现闪烁的效果。
  • GPIO_Input,为输入模式,可读取当前引脚的电平状态,该电平一般是由外部驱动的。
    GPIO_Output为输出模式,意味着可控制当前引脚的电平状态。
  1. 继续配置PC13
  • 设置GPIO output level:初始化电平,根据LED灯电路图,初始化输出低电平,灯亮。
  • 设置GPIO mode 为:Output Push Pull(推挽输出)
  • 设置 “既不上拉也不下拉”
  • 输出最大速度默认为LOW
  • 使用user label对该引脚进行别名,方便编程时区分和引用

PC13配置

项目和代码生成

单击Project Manager → Project ,配置准备要生成的工程

  • 工程名不能为中文,开头不能用数字
  • 工程路径也不能包含中文
  • 程序结构选basic即可,Advanced为高级的,工程会很大
  • IDE选择MDK-ARM
  • 设置Code Generator

设置Code Generator

上述的配置都设置好后就可以单击右上角的GENERATE CODE生成工程了。

  • 可直接Open Project,cubeMX会打开keil。

如果使用Stm32CubeIDE

更简单了,设置好芯片之后,Ctrl+S保存配置,IDE会自动生成初始代码,并跳转到main.c页面。编写好代码之后,点击顶栏的小锤子进行编译。

CubeIDE的代码页面

可能遇到的一些问题:

  1. STM32CubeIDE更新STLINK驱动失败

使用STM32 ST-LINK utility进行驱动更新

  1. Please choose another workspace as ‘E:/eclipse-workspace‘ is currently in use.
  • 删掉该workspace目录下隐藏文件.metadata中的.lock文件

如何批量注释代码

加入//的方法,整行或者多行选中,按键Ctrl + 按键/

点灯——基础版

参考教程

点灯的原理

由该最小系统板的原理图可知,PC13自带可编程普通LED灯,板载LED灯的电路:

板载LED灯

Keli5编写代码

打开工程后,进入Keli5软件,依次打开工程结构树,双击main.c开始写源码

  • 切忌:一定要在BEGIN END里写代码,不然后面STM32CubeMX重新配置生成代码, 会删除BEGIN END外面的代码

keil5界面

  • 找到Drivers文件夹里的stm32f1xx_hal_gpio.c(双击打开),找到第465行的HAL_GPIO_WritrPin函数,复制其函数名,并在main方法里的wihile(1)调用

    • 注意使用了别名LED0后,可直接在里面传入参数LED0_GPIO_Port, LED0_Pin, GPIO_PIN_RESET
      • 参数1:GPIO端口;
        参数2:GPIO引脚;
        参数3:输出电平,使LED灯亮,输出低电平
      • GPIO_PIN_RESET : 代表低电平
      • GPIO_PIN_SET : 代表高电平
  • 左上角编译,0错误,0警告

CubeIDE 编写代码

使用Alt+/可以打开代码提示,方便查找需要的函数。

函数介绍

  • void HAL_GPIO_TogglePin(GPIO_TypeDef GPIOx, uint16_t GPIO_Pin):
    这个函数用于反转(切换)指定的GPIO引脚的输出状态。它的参数包括:

    • GPIOx:GPIO端口的基址,指明要操作的GPIO端口(如GPIOA、GPIOB等)。
    • GPIO_Pin:指定要反转状态的GPIO引脚(可以是一个或多个引脚的位域)。
      • 例如,如果之前引脚是高电平,调用这个函数后引脚将变为低电平,反之亦然。
    • 由于系统运行太快,想看到LED灯闪烁,要加上延时函数:
      1
      void `HAL_Delay`(uint32_t Delay)
      此函数为毫秒级延时。如果想要指定间隔时间控制LED灯的亮灭,也是使用这个延时函数。
  • void HAL_GPIO_WritePin(GPIO_TypeDef GPIOx, uint16_t GPIO_Pin, GPIO_PinState PinState):
    这个函数用于设置指定GPIO引脚的输出状态。它的参数包括:

    • GPIOx:GPIO端口的基址,指明要操作的GPIO端口(如GPIOA、GPIOB等)。
    • GPIO_Pin:指定要设置状态的GPIO引脚(可以是一个或多个引脚的位域)。
      -PinState:指定要设置的状态,可以是 GPIO_PIN_SET(设置为高电平)或 GPIO_PIN_RESET(设置为低电平)。
  • 一种走马灯的思路设计:
    (设置状态机,通过不同状态if的设置达到多个小灯交替变换的效果,一个周期结束之后,状态清零,继续while循环)

走马灯

build

build成功

程序烧录

(之前用的keli5,后面换平台了)
这里记录的是使用CubeIDE平台,通过stlink进行程序烧录。

程序烧录成功

点灯——按键控制

此项目是为了学习GPIO的输入: GPIO_Input

关于按键——通过轮询方式读取GPIO状态

  1. 关于按键消抖

并联电容实现按键消抖

  • 上述是属于硬件消抖,但是为了以防万一,一般还会进行软件消抖——进行Delay,一般为10ms。
  1. 关于上拉:使用电源使GPIO口处的电平拉高。

上拉操作

  • 对于上拉电路:
    • 按键摁下——低电平
    • 按键松开——高电平
  1. 关于下拉:使用电源使GPIO口处的电平拉低。

上拉电路和下拉电路

  • 对于下拉电路:
    • 按键摁下——高电平
    • 按键松开——低电平
  1. stm32内置了上拉和下拉电阻:
    默认为no-pull-up and no-pull-down :浮空输入模式
  • 上拉输入模式:Pull-up
  • 下拉输入模式:Pull-down

stm32内置GPIO上拉输入和下拉输入

函数介绍

  1. GPIO_PinState HAL_GPIO_ReadPin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
  • 参数:
    • GPIOx:GPIO 控制器的基址,例如 GPIOA、GPIOB 等。
    • GPIO_Pin:要读取状态的 GPIO 引脚,可以使用宏定义,如 GPIO_PIN_0、GPIO_PIN_1 等。
  • 返回值:
    返回值类型为 GPIO_PinState,该类型是一个枚举类型,有两个可能的值:
    • GPIO_PIN_RESET:引脚处于低电平状态。
    • GPIO_PIN_SET:引脚处于高电平状态。

用法:

1
2
3
4
5
6
7
8
9
GPIO_PinState pinState;
pinState = HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0);
if(pinState == GPIO_PIN_SET) {
// 如果引脚处于高电平状态,则执行相应操作
//例如:熄灭小灯
} else {
// 如果引脚处于低电平状态,则执行相应操作
//例如:点亮小灯
}
  1. 按键反转小灯亮灭:

stm32内置GPIO上拉输入和下拉输入

需要添加按键摁下时等待,否则小灯会一直在while循环中从低电平被拉到高电平,然后又由于按键下拉成为低电平。

stm32内置GPIO上拉输入和下拉输入

使用矩阵式键盘模块

基本原理

博主使用的矩阵式键盘模块实物图

矩阵按键扫描原理:

  • 先是把列(col)置0(推挽输出),行是输入上拉,扫描行得到行的键值;
  • 再是把行(row)置0(推完输出),列是输入上拉,扫描列得到列的键值;
  • 最后把行列的键值相加得到最后的总的键值。

矩阵式键盘模块原理图

关于GPIO配置

4个引脚配推挽输出,这4个配输出的引脚内部上下拉不用配置;另外4个配成输入,内部上拉。

矩阵式键盘模块GPIO配置

在key.h里面对硬件的引脚进行define,方便后面代码的书写。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include "stm32f1xx_hal.h"
#include "gpio.h" //这里如果不引入gpio.h,会出现报错显示引脚标签为没有定义的变量
#ifndef _KEY_BOARD_H_
#define _KEY_BOARD_H_


#define KEYBOARD_GPIO_PORT GPIOD
#define KEYBOARD_GPIO_CLK_FUN RCC_APB2PeriphClockCmd
#define KEYBOARD_GPIO_CLK RCC_APB2Periph_GPIOD


//line 行
#define KEYBOARD_GPIO_PIN0 KEY_col0_Pin
#define KEYBOARD_GPIO_PIN1 KEY_col1_Pin
#define KEYBOARD_GPIO_PIN2 KEY_col2_Pin
#define KEYBOARD_GPIO_PIN3 KEY_col3_Pin



//row 列
#define KEYBOARD_GPIO_PIN4 KEY_row0_Pin
#define KEYBOARD_GPIO_PIN5 KEY_row1_Pin
#define KEYBOARD_GPIO_PIN6 KEY_row2_Pin


//extern uint8_t Send_F;

extern void Keyboard_GPIO_Config(void);
extern uint16_t keyboard_scan(void);
#endif

具体的函数实现在key.c文件中,具体的调用方法参见库函数整理的那一篇博客。

关于GPIO的内部电路原理

GPIO功能描述

每个GPI/O端口有:
两个32位配置寄存器(GPIOx_CRL,GPIOx_CRH),两个32位数据寄存器(GPIOx_IDR和GPIOx_ODR),一个32位置位/复位寄存器(GPIOx_BSRR),一个16位复位寄存器(GPIOx_BRR)和一个32位锁定寄存器(GPIOx_LCKR)。
根据数据手册中列出的每个I/O端口的特定硬件特征, GPIO端口的每个位可以由软件分别配置成多种模式。

  • 输入浮空
  • 输入上拉
  • 输入下拉
  • 模拟输入
  • 开漏输出
  • 推挽式输出
  • 推挽式复用功能
  • 开漏复用功能

GPIO口读取高低电平原理

  • 模拟输入,读取的是具体数值,没有经过施密特触发器处理。
  • 数字输入和复用输入都是读取的高低电平(经过施密特触发器过滤处理)

I/O端口位的基本结构

1.输出驱动器:
主要配置开漏输出推挽输出

开漏输出和推挽输出区别

  • 推挽输出:
    该模式需要P-MOS和N-MOS协同作用。

推挽输出-输出高电平

推挽输出-输出低电平

  • 开漏输出:
    当外设元器件需要的电压不是3.3V时,可以使用开漏输出,此时只有N-MOS工作,外接GND变为器件需要的工作电压VCC。
    • 开漏输出必须依靠外接的电压源来进行驱动,当外接电压大于3.3V时,需要选择5V容忍的I/O口。
    • 当要求I/O口输出低电平时,N-MOS断开,反之则N-MOS连接GND为0V。

开漏输出

  • 由于输出驱动器的控制指令来源有2个:
    • 一个是写入(HAL_GPIO_WritePin函数)控制的输出寄存器
    • 另一个是片上外设,例如串口模块,I^2C模块的复用功能输入

因此,根据控制来源的不同,stm32将输出模块分为:

  • 开漏输出
  • 推挽式输出
  • 推挽式复用功能
  • 开漏复用功能
  1. 输入驱动器:
    根据是否开启上拉/下拉电阻可以选择以下三种模式。
  • 输入浮空
  • 输入上拉
  • 输入下拉

TTL肖特基触发器的功能:稳定电平

TTL肖特基触发器

经过肖特基触发器处理之后的电平数据存储在输入数据寄存器中,等待使用HAL_GPIO_ReadPin函数对寄存器进行读取。

  • 模拟输入模式可以读取输入电压的具体数值,即不经过肖特基触发器处理的原始电压数据。

  • 输入部分的不同分支可以同时读取施密特触发器的输出:

输入部分

中断

中断的概念:

输入部分

常见的中断事件有:

  • 指令出错
  • 定时器结束
  • 串口接收数据
  • GPIO电平发生变化(外部中断EXTI)

外部中断:红灯循环闪烁,按键中断,绿灯闪烁

不使用中断,会存在的情况:

逻辑错误

设置中断:

将接收电平输入的引脚设置为:GPIO_EXTIXX,XX为引脚号,表示第XX号外部中断线。

  • 设置GPIO mode:
    • 上升沿触发中断:某个GPIO口读取的电平由低电平变为高电平,触发中断
    • 下降沿触发中断:某个GPIO口读取的电平由高电平变为低电平,触发中断
    • 上升、下降沿都触发中断

GPIO mode

  • 设置NVIC(中断控制器):勾选开启中断向量EXTI15_10

代码编写

在Core/Src中可以找到stm32f1xx_it.c,其后缀it就表示它是与中断(interrupt)相关的文件。

此文件的最底部有CubeMX自动生成的函数:

1
2
3
void EXTI15_10_IRQHandler(void){
HAL_GPIO_EXTI_IRQHandler(KET1_PIN); # KET1_PIN为引脚的用户标签
}

该函数为我们摁下按键触发中断后,stm32会调用执行的中断处理函数。

外部中断的软件消抖(按键)

软件消抖

  • 在中断处理函数中,先等10ms,用于软件消抖,等按键稳定之后,再读取按键状态判断是否为有效的中断触发。

    • 若为误触,则中断处理函数不做任何操作,直接回到主程序
  • 注意:如果要在回调函数中使用HAL_Delay(),就必须配置中断优先级:

中断优先级配置

  • System Core -> NVIC,将 Time base: System tick timer 的主要优先级调到比EXTI line[15:10] Interrupts 高即可。
    • HAL_Delay()函数依靠System tick timer的中断提供1ms的时钟基准。该中断的优先级如果低于我们触发的中断,使 HAL_Delay() 函数无法在中断函数中正常执行,会导致程序卡死在这里。

补充

在正规的项目中,在中断中调用HAL_Delay()函数是不被推荐的,需要尽可能的保证中断任务可以尽快执行完成,将中断对正常执行流程的影响降到最低。

深入外部中断

EXTI

外部中断/事件控制器框图

在stm32f1系列芯片中一共有19个这样的外部中断/事件控制器,它们共用一套寄存器,但连线是独立的,称为外部中断线

  • 前16条外部中断线,即EXTI0~EXTI15分别对应其同号的GPIO口。
    • 从PA0,PB0,PC0,PD0进入的电平信号都可以进入EXTI0。
  1. 事件信号:
    事件是和中断类似的概念,但是事件信号直接送达相应的外设,由外设自行处理,不会中断正常代码执行流程。

事件信号处理部分

  1. 软件中断事件寄存器可以用代码模拟产生一个中断。
  2. 边沿检测电路就是用来检测电平变化的,经过一个或门,传输中断数据到请求挂起寄存器,其输出的信号和中断屏蔽寄存器的信号通过一个与门, 进入NVIC中断控制器。

NVIC 嵌套向量中断控制器

  1. 关于中断向量:
  • EXTI0~EXTI4 : 拥有自己的中断向量
  • EXTI5~EXTI9: 共享中断向量 EXTI9_5
  • EXTI10~EXTI15: 共享中断向量EXTI15_10

中断向量

  1. NVIC会一直读取中断信号线是否存在中断请求。为了让中断处理函数只执行一遍而不是无限重复,需要在中断处理函数中,将请求挂起寄存器的对应位清零。
    • CubeMX生成的代码中,中断处理函数里面自动调用了该清零的函数_HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN)。
      所以不需要额外写。
  2. 优先级
  • 两中断同时发生时,先比较抢占优先级;若抢占优先级相同,再比较响应优先级
  • 某中断正在进行中,另一中断突然发生,只比较二者抢占优先级。

CubeMX用四个二进制位表示优先级,默认4位均为抢占优先级。

一些其他的中断

  • 串口USART:
    • 空暇中断
    • 接收中断
    • 发送完成中断
    • 奇偶校验中断
  • 定时器TIM:
    • 刹车中断
    • 触发和通信中断
    • 捕获比较中断
  • I^2C:
    • 传输完成终端
    • 地址发送终端
    • 起始位发送中断
    • 接收缓存区非空中断

这些中断虽然没有外部中断线这套结构,但是也有相关的请求挂起寄存器和中断屏蔽寄存器,触发中断后依旧需要NVIC通过中断向量找到并执行中断处理函数。

串口通信——TTL串口

常见的串口:

  • RS-232、RS-485
  • RJ-45 :网线接口
  • USB串口

这里介绍的是单片机中最常见的串口:TTL串口

串口接线图

  • 共地是两设备通信的前提
  • TTL串口为异步通信
  • 波特率:每秒传送的码元数量,即每秒多少次高低电平信号。
    • TTL串口每传递一个字节(Byte),也就是8 bit数据,加上一位起始位和一位停止位,每传递一字节的信息需要10 bit
    • 常见的波特率:
      • 115200
      • 9600
      • 19200
      • 38400
    • 通信的两个设备需要使用相同的波特率才能正常通信。
  • 除了波特率,另外需要在CubeMX中设置的Word Length(字节长度),Parity(校验位),Stop Bits(停止位)保持默认即可。

初识串口——项目实战

串口通信方式:

  • 轮询方式:CPU不断检测串口的状态标志来判断数据收发的情况。
    • 特点:程序设计简单,但CPU在检测标志位期间,无法执行其他任务,CPU利用率较低。
  • 中断方式:使能中断后,接收一字节数据或发送一字节后申请中断,在ISR中完成后续处理。
    • 在数据收发期间,CPU可以执行其他任务,CPU利用率较高。
  • DMA方式:初始化时设置相关参数,启动DMA传输后,数据传输过程不需要CPU的干预。
    • 传输完成后,再产生DMA中断,由CPU进行后续处理,传输效率最高。

轮询模式的底层机制

在stm32的每个串口内部都有两个发送数据寄存器:发送数据寄存器(TDR),发送移位寄存器,两个接收数据的寄存器:接收数据寄存器(RDR),接收移位寄存器

两个发送数据寄存器的作用

CPU不停的轮询发送数据寄存器中的数据是否已经移送到发送移位寄存器。

同理,CPU不停的轮询接收数据寄存器中是否有新数据可以读,直到接收完设置的希望接收的字节数或者时间超时。

  • 在轮询模式下,不管是发送还是接收,CPU一直处于忙碌状态。容易产生长期串口占用CPU导致堵塞的问题。

CubeMX设置

stm32F103C8T6的芯片中,调试串口的I/O口为PA2和PA3。
设置USART2为异步模式,设置波特率为115200,保存设置并生成代码。

CubeMX设置串口

代码编写

函数介绍:串口轮询模式

  1. MX_USART2_UART_Init():初始化USART2串口。
    这个函数通常在微控制器固件的初始化阶段被调用,用于根据在 STM32CubeMX 中配置的参数(如波特率、数据位、停止位和校验位)设置和配置 USART2 外设(在本例中为 UART2)。

  2. 发送数据 :

    1
    HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout);
  • 参数:
    • huart:指向 UART 外设的句柄的指针,包含了 UART 的配置和状态信息。
    • pData:指向要发送的数据缓冲区的指针。
    • Size:要发送的数据字节数。
    • Timeout:发送操作的超时时间(以毫秒为单位)。如果在超时时间内未完成发送操作,则函数可能会返回超时错误。
      • HAL_MAX_DELAY :表示不设超时时间,可以无限等待到发送完成。
  • 返回值:
    • HAL_OK:操作成功完成。
    • 其他错误代码,例如超时错误或者传输中断。
  • 功能:
    • 当调用 HAL_UART_Transmit() 函数时,它会尝试将指定数量的数据字节发送到 UART 外设。
    • 如果 UART 外设正在发送其他数据或者处于忙状态,该函数将等待直到 UART 外设空闲,然后再发送数据。
    • 如果发送过程中发生错误(例如超时或者传输中断),函数将返回相应的错误代码。

实战项目:串口控制LED灯的亮灭

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
int main(void)
{
/* MCU Configuration--------------------------------------------------------*/
HAL_Init();
SystemClock_Config();
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_USART2_UART_Init();

/* USER CODE BEGIN 2 */
char message_1[]="Normal situation : The LED is extincting ...";
char message_2[]="The LED is lit up !!!";
uint8_t receiveData[1];
/* USER CODE END 2 */

/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
HAL_UART_Receive(&huart2,receiveData,1,100);
//Normal situation--Flashing light
HAL_GPIO_WritePin(LED_0_GPIO_Port, LED_0_Pin, GPIO_PIN_RESET);
HAL_Delay(1000);
if(receiveData[0]=='0'){
HAL_GPIO_WritePin(LED_0_GPIO_Port, LED_0_Pin, GPIO_PIN_RESET);
HAL_UART_Transmit(&huart2,(uint8_t*)message_2,strlen(message_2),100);
HAL_Delay(5000);
}
else{
HAL_UART_Transmit(&huart2,(uint8_t*)message_1,strlen(message_1),100);
HAL_GPIO_WritePin(LED_0_GPIO_Port, LED_0_Pin, GPIO_PIN_SET);
HAL_Delay(1000);
}
receiveData[0]=1;

/* USER CODE END WHILE */
}
}

串口的中断模式

采用中断模式,当CPU把发送的内容塞入发送数据寄存器之后,就去处理其他代码,当发送数据寄存器为空之后,触发中断,CPU再继续塞数据…由此可以解决串口占用CPU导致的阻塞问题。

串口的中断模式

CubeMX设置

在NVIC中勾选上UART2即可。

串口中断模式设置

函数介绍

  1. 发送数据:HAL_UART_Transmit_IT()
    1
    HAL_StatusTypeDef HAL_UART_Transmit_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);
  • 参数:
    • huart:指向 UART 外设的句柄的指针,包含了 UART 的配置和状态信息。
    • pData:指向要发送的数据缓冲区的指针。
    • Size:要发送的数据字节数。
    • 因为是中断模式,不会长期占用CPU,所以无需设置等待时间
  • 功能:
    • 当调用 HAL_UART_Transmit_IT() 函数时,它会将指定数量的数据字节放入发送缓冲区,并启动发送过程。
    • 函数将立即返回,不会等待数据发送完成。数据将在后台通过中断的方式进行发送。
    • 一旦发送完成,将触发相应的 UART 发送完成中断,可以在中断处理函数中进行进一步的处理。
  1. 接收数据:HAL_UART_Receive_IT()
    1
    HAL_StatusTypeDef HAL_UART_Receive_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);
  • 参数:
    • huart:指向 UART 外设的句柄的指针,包含了 UART 的配置和状态信息。
    • pData:指向接收数据的缓冲区的指针。
    • Size:要接收的数据字节数。
  • 功能:
    • 当调用 HAL_UART_Receive_IT() 函数时,它会启动接收过程,并将接收到的数据存储到指定的缓冲区中。
    • 函数将立即返回,不会等待数据接收完成。数据将在后台通过中断的方式进行接收。
      • 因此不能把这个函数放到while循环中,否则会出现新一轮循环调用它时,上一次的接收还未结束
      • 需要设置回调函数,并且记得在HAL_UART_RxCpltCallback()中开启下一次串口的接收
    • 一旦接收到指定数量的数据字节或者接收超时,将触发相应的 UART 接收完成中断(接收数据寄存器非空中断),可以在中断处理函数中获取接收到的数据。
  1. 回调函数

USART中断请求

一个USART只有一个中断向量,使用回调函数,可以更好的处理各种中断事件。

  • HAL_UART_RxCpltCallback()函数
    当串口接收到指定数量的数据字节后,将触发接收完成中断。此时,HAL 库将自动调用 HAL_UART_RxCpltCallback() 函数(如果已经在代码中定义了该函数),并将 UART 句柄作为参数传递给它。
    此类函数虽然定义在stm32的库函数中,但是都是_weak定义,可以在其他地方重新定义此函数,然后在中间加入处理部分的代码。
    • 一般单开一个文件对这类函数重新定义的。
      通常情况下,可以在 HAL_UART_RxCpltCallback() 函数中进行以下操作:
    • 处理接收到的数据,例如解析数据包、执行相应的操作或者存储数据到缓冲区中。
    • 启动下一次接收过程,以便继续接收数据。

示例:

1
2
3
4
5
6
7
8
9
10
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
if (huart == &huart2) {
// 在这里处理接收到的数据,例如打印到终端或者执行特定的操作
// 以下示例假设接收到的数据存储在 rxBuffer 中,并且数据长度为 RX_BUFFER_SIZE
printf("Received data: %s\n", rxBuffer);

// 启动下一次接收过程
HAL_UART_Receive_IT(&huart2, rxBuffer, RX_BUFFER_SIZE);
}
}
  1. 补充一个代码规范:用户自定义的全局变量,放在USER CODE PV注释块中

串口DMA模式和接收不定长数据

DMA的概念

DMA(Direct Memory Access,直接内存访问)是一种计算机系统中用于实现高效数据传输的技术。它允许外部设备(如网络适配器、硬盘控制器、串口等)直接访问系统内存,而无需 CPU 的干预。DMA 可以大大提高数据传输的效率,因为它可以在数据传输过程中释放 CPU,使 CPU 可以同时执行其他任务。

  • 数据传输效率:DMA 允许数据在外设和内存之间直接传输,无需 CPU 的介入。这消除了 CPU 在数据传输期间的等待时间,从而提高了系统的总体效率。

  • 减少 CPU 负载:由于 DMA 可以独立地管理数据传输,CPU 可以将更多的时间用于执行其他任务,从而减少了 CPU 的负载。这对于实时系统和多任务系统尤其有用。

  • 多通道支持:许多 DMA 控制器支持多个 DMA 通道,每个通道可以独立地管理一个数据传输任务。这允许系统同时执行多个数据传输,提高了系统的并发性。

形象理解DMA

  • 工作原理:在 DMA 操作中,CPU 配置 DMA 控制器来指定数据传输的源地址、目的地址和数据长度等参数。一旦 DMA 控制器被配置好,它可以独立地控制数据传输,直到传输完成或者遇到错误。一旦数据传输完成,DMA 控制器触发一个DMA传输完成中断或者通知 CPU。

  • 适用场景:DMA 主要用于大量数据的传输,例如大文件的读写、图像传输、音频处理等场景。

中断模式 发送数据寄存器空终端、接收数据寄存器非空中断 每收/发一个字节时
DMA模式 DMA传输完成中断 收/发完成时

四种情况的数据传输如下:

Dirction : DMA传输方向
(1)外设到内存 Peripheral To Memory
(2)内存到外设 Memory To Peripheral
(3)内存到内存 Memory To Memory
(4)外设到外设 Peripheral To Peripheral

DMA基础工作原理

当用户将参数设置好,主要涉及源地址目标地址传输数据量这三个,DMA控制器就会启动数据传输,当剩余传输数据量为0时 达到传输终点,结束DMA传输。

  • 当然,DMA 还有循环传输模式 当到达传输终点时会重新启动DMA传输。
    • 也就是说只要剩余传输数据量不是0,而且DMA是启动状态,那么就会发生数据传输。

     

  • 总之,每次DMA传送由3个操作组成:
    • 从外设数据寄存器或者从当前外设/存储器地址寄存器指示的存储器地址取数据,第一次传输时的开始地址是DMA_CPARx或DMA_CMARx寄存器指定的外设基地址或存储器单元;
    • 存数据到外设数据寄存器或者当前外设/存储器地址寄存器指示的存储器地址,第一次传输时的开始地址是DMA_CPARx或DMA_CMARx寄存器指定的外设基地址或存储器单元;
    • 执行一次DMA_CNDTRx寄存器的递减操作,该寄存器包含未完成的操作数目

DMA传输方式

  • 方法1:DMA_Mode_Normal,正常模式
    当一次DMA数据传输完后,停止DMA传送 ,也就是只传输一次
  • 方法2:DMA_Mode_Circular ,循环传输模式
    当传输结束时,硬件自动会将传输数据量寄存器进行重装,进行下一轮的数据传输。 也就是多次传输模式

CubeMX配置DMA

CubeMX配置DMA

详细参考配置教程

  • DMA Request : DMA传输的对应外设
    • 注意: 如果你是在DMA设置界面添加DMA 而没有开启对应外设的话 ,默认为MENTOMEN
  • Channel DMA传输通道设置:
    DMA1 : DMA1 Channel 0 - DMA1 Channel 7
    DMA2: DMA2 Channel 1 - DMA1 Channel 5
  • Priority: 传输速度
    最高优先级 Very Hight
    高优先级 Hight
    中等优先级 Medium
    低优先级;Low
  • DMA指针递增设置
    Increment Address:地址指针递增。
    (1)左侧Src Memory 表示外设地址寄存器
    功能:设置传输数据的时候外设地址是不变还是递增。如果设置为递增,那么下一次传输的时候地址加 Data Width个字节。
    (2)右侧Dst Memory 表示内存地址寄存器
    功能:设置传输数据时候内存地址是否递增。如果设置为递增,那么下一次传输的时候地址加 Data Width个字节。

DMA指针递增设置

  • 串口发送数据是将数据不断存进固定外设地址串口的发送数据寄存器(USARTx_TDR)。所以外设的地址是不递增。
  • 而内存储器存储的是要发送的数据,所以地址指针要递增,保证数据依次被发出

DMA指针递增原理

  • 串口数据发送寄存器只能存储8bit,每次发送一个字节,所以数据长度选择Byte

函数介绍

  • HAL_UART_Transmit_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)
    串口DMA模式发送
  • HAL_UART_Receive_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)
    串口DMA模式接收

前两个函数的形参和串口中断模式一样。

  • HAL_UART_DMAPause(&huart1)
    暂停串口DMA

  • HAL_UART_DMAResume(&huart1)
    恢复串口DMA

    • 作用: 恢复DMA的传输
    • 返回值: 0 正在恢复 1 完成DMA恢复
  • HAL_UART_DMAStop(&huart1)
    结束串口DMA

STM32 IDLE 接收空闲中断(可接收不定长数据)

如果勾选了为每个外设生成单独的.c.h文件,在按照视频关闭dma半传输中断时会报错,需要在代码的开头加上:

1
extern DMA_HandleTypeDef hdma_usart2_rx;

我们可以认为空闲中断发生时,一帧数据包就接收完成了。

函数介绍

  1. 接收数据函数
    1
    HAL_StatusTypeDef HAL_UARTEx_ReceiveToIdle_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);
  • uint16_t Size为可接收的最大数据size,一般可以直接sizeof(receiveData)
  1. 回调函数
    1
    void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
  • 使用回调函数的一个好习惯:先确认是哪个串口触发了回调函数
    1
    2
    3
    if(huart == &huart2){
    \\确认是需要处理的目标串口之后再进行函数处理
    }
  1. 接收数据储存在receiveData数组中,在回调函数中处理之后,需要清零数组内容,以便下一次接收数据。
    1
    memset(receiveData, 0, sizeof(receiveData));

项目实战:

需求:接收电脑端发送的三种类型的数据:1,非1的单个数据,长数据

  • 收到1,发送The LED Is Lit Up!,点亮小灯
  • 收到非1的单个数据,发送Get Wrong Messages ~ Why Not TRY again?
  • 收到长数据,发送Get Long Messages ~YOU SEND :这里拼接上电脑端发送的长数据
  • while主程序为闪烁小灯
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    uint8_t receiveData[50];
    volatile uint8_t tx_complete = 1;
    void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart){
    if (huart->Instance == USART2) {
    tx_complete = 1;
    }
    }
    //串口空闲中断的回调函数
    void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
    {
    if(huart == &huart2){
    //先确认是哪个串口触发了回调函数
    if(Size >1){
    //接收到长数据
    char message_long[100]="Get Long Messages ~YOU SEND :";
    strcat(message_long,receiveData);
    if (tx_complete) {
    tx_complete = 0;
    HAL_UART_Transmit_DMA(&huart2,(uint8_t*)message_long,strlen(message_long));
    }
    memset(receiveData, 0, sizeof(receiveData));
    }
    else{
    //接收到短数据
    char message_1[]="Get Wrong Messages ~ Why Not TRY again?";
    char message_2[]="The LED Is Lit Up!";
    if (tx_complete) {
    tx_complete = 0;
    if(receiveData[0]=='1'){
    HAL_GPIO_WritePin(LED_0_GPIO_Port, LED_0_Pin, GPIO_PIN_RESET);
    HAL_UART_Transmit_DMA(&huart2,(uint8_t*)message_2,strlen(message_2));
    }
    else
    HAL_UART_Transmit_DMA(&huart2,(uint8_t*)message_1,strlen(message_1));
    }
    receiveData[0]=0;
    }

    //记得开启下一次的串口接收函数
    HAL_UARTEx_ReceiveToIdle_DMA(&huart2,receiveData,sizeof(receiveData));
    }
    }

    int main(void)
    {
    /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
    HAL_Init();
    /* Configure the system clock */
    SystemClock_Config();

    /* Initialize all configured peripherals */
    MX_GPIO_Init();
    MX_DMA_Init();
    MX_USART2_UART_Init();

    HAL_UARTEx_ReceiveToIdle_DMA(&huart2,receiveData,sizeof(receiveData));


    while (1)
    {
    //Normal situation--Flashing light
    char message_3[]="Normal situation : The LED is extincting ...";
    HAL_GPIO_WritePin(LED_0_GPIO_Port, LED_0_Pin, GPIO_PIN_RESET);
    HAL_Delay(200);
    HAL_GPIO_WritePin(LED_0_GPIO_Port, LED_0_Pin, GPIO_PIN_SET);
    HAL_Delay(200);
    if (tx_complete) {
    tx_complete = 0;
    HAL_UART_Transmit_DMA(&huart2,(uint8_t*)message_3,strlen(message_3));
    }
    }
    }

简单帧格式串口通信

https://blog.csdn.net/weixin_65489379/article/details/122792920

简单的帧格式

常见的帧格式为:
帧头-帧size-数据部分-校验位-帧尾

解析数据包

解析数据包就是对收到的数据内部按照帧格式进行解析

  • 该项目使用空闲中断DMA接收不定长数据,使用使用简单帧格式对三色LED灯进行控制,实现命令某几种颜色LED灯闪烁,某几种颜色的灯长时间熄灭的效果。
  • 采用的简单帧格式为:帧头(0xAA)-帧size-数据部分-和校验
  • 该项目的不足:在相对复杂的工程中,需要建立数据缓冲区,将解析数据包的步骤搬出中断,在主循环中进行,并且需要考虑数据粘包数据丢失等情况。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
    {
    uint8_t Verify_Flag =0;
    //接收新消息之前需要重置FLAG
    Red_Flag = 0;
    Green_Flag = 0;
    Yello_Flag = 0;
    if(huart == &huart2){
    //不定长数据接收(控制外设的3色LED灯)
    if(Size >1){
    char message_long[100]="Get Long Messages ~";
    // 开始校验数据
    if(receiveData[0]== 0xAA )//帧头校验
    if(receiveData[1]== Size){//位数校验
    uint8_t sum = 0;
    for(int i=0;i<Size-1;i++){
    sum+=receiveData[i];
    }
    if(sum==receiveData[Size-1]){
    //和校验
    Verify_Flag =1;
    }
    }
    if(Verify_Flag){
    //校验成功,开始解析数据
    for(int i=2;i<Size-1;i+=2){
    uint8_t state = 0;
    if(receiveData[i+1]== 0xFF)
    state = 1;
    if(receiveData[i]==0x01)//red
    Red_Flag =state;
    else if(receiveData[i]==0x02)//yello
    Yello_Flag =state;
    else if(receiveData[i]==0x03)//green
    Green_Flag =state;
    }
    }
    if (tx_complete) {
    tx_complete = 0;
    HAL_UART_Transmit_DMA(&huart2,(uint8_t*)message_long,strlen(message_long));
    }
    memset(receiveData, 0, sizeof(receiveData));
    }
    //定长单个指令接受(控制LED_0)
    else{
    char message_1[]="Get Wrong Messages ~ Why Not TRY again?";
    char message_2[]="The LED Is Lit Up!";
    if (tx_complete) {
    tx_complete = 0;
    if(receiveData[0]=='1'){
    HAL_GPIO_WritePin(LED_0_GPIO_Port, LED_0_Pin, GPIO_PIN_RESET);
    HAL_UART_Transmit_DMA(&huart2,(uint8_t*)message_2,strlen(message_2));
    }
    else
    HAL_UART_Transmit_DMA(&huart2,(uint8_t*)message_1,strlen(message_1));
    }
    receiveData[0]=0;
    }
    HAL_UARTEx_ReceiveToIdle_DMA(&huart2,receiveData,sizeof(receiveData));
    }
    }

蓝牙模式

基本概念

  1. 蓝牙是一种短距的无线通讯技术,可实现固定设备、移动设备之间的数据交换。一般将蓝牙3.0之前的BR/EDR蓝牙称为传统蓝牙,而将蓝牙4.0规范下的LE蓝牙称为低功耗蓝牙

  2. BLE(Bluetooh Low Energy)蓝牙低能耗技术是短距离、低成本、可互操作性的无线技术,它利用许多智能手段最大限度地降低功耗。

    BLE技术的工作模式非常适合用于从微型无线传感器(每半秒交换一次数据)或使用完全异步通信的遥控器等其它外设传送数据。这些设备发送的数据量非常少(通常几个字节),而且发送次数也很少(例如每秒几次到每分钟一次,甚至更少)。

  3. 协议概述
    所谓协议,即将指定的字节按照一定的顺序排列起来,以便他人使用自己的设备时,能通过该协议同其他设备进行通信。协议一特点,就是有固定的帧格式,通过该格式发送,接收者通过解读帧格式,进而得到新息内容;

  • 一般通信协议,一类通信是直接发生数据,当设备接送到数据时,直接对数据进行解析,当接受到的数据合法时,即为有效数据,该类型的通信协议,主要用在有线通信协议中,比如Modbus,Can通常采用的即为该类型的通信方式。

蓝牙BLE协议

  • 另一类通信协议,则需要新建立连接,当双方连接建立成功了方可通信,例如TCP、BLE;BLE协议在需要进行通信时,即需要向外发送广播信号,告诉接收者,即将和它进行通信,接受者接收到广播内容后,确认是与自己通信,于是向广播者发送一响应信息,这样当广播者和接受者都有了对方的身份信息时,即表示双方连接成功。
    • 因此,在连接过程中,必定有相应的广播帧格式。在BLE通信过程中,假设设备A需要连其他设备假设为B,则A需要不断地发送广播信号(此过程一般有一个时间间隔,在没发送广播数据时间内,芯片处于低功耗状态),每发送一次广播包,称之为一次广播事件。

IIC通信

IIC通信概念

IIC通信

  • 串口通信是全双工通信,除了共GND外,两根线(TX,RX)都能同时传输数据
    • 异步通信模式,双方统一比特率,在合适时机设置或者读取数据线上的高低电平
  • IIC通信是半双工通信,采用主从模式进行单线通信,必须先由主机向从机发送数据(询问)之后从机才能应答。
    • 传输数据的线为SDA线,另一根线为SCL线,提供同步时钟脉冲。
      • 同步通信
    • 可以通过ICC实现多设备通信。

未完待续


STM32F103C8T6最小系统板(电控)学习指南
http://zoechen04616.github.io/2024/03/03/STM32F103C8T6最小系统板-电控-学习指南/
作者
Yunru Chen
发布于
2024年3月3日
许可协议