LED 状态说明

  • 闪烁红色:ST-LINK/V2 连接到计算机后,第一次 USB 枚举过程
  • 红色:ST-LINK/V2 与计算机已建立连接
  • 闪烁绿色/红色:目标板和计算机在进行数据交换
  • 绿色:通讯完成
  • 橙色(红色+绿色):通讯失败

接口定义

SWIM 接口定义

  • 连接至 STM8 系列微控制器
  • 由于 STM8 的 SWIM 接口只需要 4 根连接线,所以 ST-LINK/V2 连接至 STM8 目标板时需要注意连接位置
  • SWIM模式下支持1.65V ~ 5.5V应用电压,支持低速(9.7 Kbytes/s)和高速模式(12.8 Kbytes/s)编程,可使用 ERNI 标准的垂直/水平连接器,以及 2.54 毫米的连接插座
仿真器端口 连接目标板 功能
1. VDD MCU VCC 连接 STM8 目标板的电源 VCC
2. DATA MCU SWIM pin 连接 STM8 目标板的 SWIM PIN
3. GND GND 连接 STM8 目标板的电源 GND
4. RESET MCU RESET pin 连接 STM8 目标板的 RESET PIN

img

JTAG/SWD 接口

  • 连接至 STM32 系列微控制器
  • JTAG/SWD模式下支持1.65V ~ 3.6V的应用电压与5V容错输入,支持间距2.54mm的 20 针 JTAG 连接器,以及 SWD 和串行线查看器(SWV)通信

img

仿真器端口 连接目标板 功能
1. TVCC MCU 电源 VCC 连接 STM32 目标板的电源 VCC
2. TVCC MCU 电源 VCC 连接 STM32 目标板的电源 VCC
3. TRST GND GROUND
4. UART-RX GND GROUND
5. TDI TDI 连接 STM32 的 JTAG TDI
6. UART-TX GND GROUND
7. TMS, SWIO TMS, SWIO 连接 STM32 的 JTAG 的 TMS, SWD 的 SW IO
8. BOOT0 GND GROUND
9. TCK, SWCLK TCK, SWCLK 连接 STM32 的 JTAG 的 TCK, SWD 的 SW CLK
10. SWIM GND GROUND
11. NC NC Unused
12. GND GND GROUND
13. TDO TDO 连接 STM32 的 JTAG TDO
14. SWIM-RST GND GROUND
15. STM32-RESET RESET 连接 STM32 目标板的 RESET 端口
16. KEY NC GROUND
17. NC NC Unused
18. GND GND GROUND
19. VDD NC VDD (3.3V)
20. GND GND GROUND

img

使用准备

boot 启动模式

  • 可以通过设置 BOOT1 和 BOOT0 引脚的状态,来选择在复位后的启动模式

  • 第一种方式(boot0 = 0):Flash memory 启动方式
    • 启动地址:0x08000000 是 STM32 内置的 Flash
    • 一般我们使用JTAG或者SWD模式下载程序时,就是下载到这个里面,重启后也直接从这启动程序。基本上都是采用这种模式
  • 第二种方式(boot0 = 1;boot1 = 0):System memory 启动方式

    • 启动地址:0x1FFF0000 从系统存储器启动,这种模式启动的程序功能是由厂家设置的
    • 一般来说,这种启动方式用的比较少。系统存储器是芯片内部一块特定的区域,STM32 在出厂时,由 ST 在这个区域内部预置了一段 BootLoader, 也就是我们常说的 ISP 程序, 这是一块 ROM,出厂后无法修改。一般来说,我们选用这种启动模式时,是为了从串口下载程序,因为在厂家提供的 BootLoader 中,提供了串口下载程序的固件,可以通过这个 BootLoader 将程序下载到系统的 Flash 中
    • 下载步骤:
      1. 将 BOOT0 设置为 1,BOOT1 设置为 0,然后按下复位键,这样才能从系统存储器启动 BootLoader
      2. 最后在 BootLoader 的帮助下,通过串口下载程序到 Flash 中
      3. 程序下载完成后,又有需要将 BOOT0 设置为 GND,手动复位,这样,STM32 才可以从 Flash 中启动可以看到, 利用串口下载程序还是比较的麻烦,需要跳帽跳来跳去的,非常的不注重用户体验
  • 第三种方式(boot0 = 1;boot1 = 1):SRAM 启动方式

    • 启动地址:0x20000000 内置 SRAM
    • 既然是 SRAM,自然也就没有程序存储的能力了,这个模式一般用于程序调试。假如我只修改了代码中一个小小的 地方,然后就需要重新擦除整个 Flash,比较的费时,可以考虑从这个模式启动代码(也就是 STM32 的内存中),用于快速的程序调试,等程序调试完成后,在将程序下载到 SRAM 中

printf

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
/* USER CODE BEGIN 0 */
#include "stdio.h"
/* 加入以下代码, 支持printf函数, 而不需要选择use MicroLIB */
#if 1

#if (__ARMCC_VERSION >= 6010050) /* 使用AC6编译器时 */
__asm(".global __use_no_semihosting\n\t"); /* 声明不使用半主机模式 */
__asm(
".global __ARM_use_no_argv \n\t"); /* AC6下需要声明main函数为无参数格式,否则部分例程可能出现半主机模式
*/

#else
/* 使用AC5编译器时, 要在这里定义__FILE 和 不使用半主机模式 */
#pragma import(__use_no_semihosting)

struct __FILE {
int handle;
/* Whatever you require here. If the only file you are using is */
/* standard output using printf() for debugging, no file handling */
/* is required. */
};

#endif

/* 不使用半主机模式,至少需要重定义_ttywrch\_sys_exit\_sys_command_string函数,以同时兼容AC6和AC5模式
*/
int _ttywrch(int ch) {
ch = ch;
return ch;
}

/* 定义_sys_exit()以避免使用半主机模式 */
void _sys_exit(int x) { x = x; }

char *_sys_command_string(char *cmd, int len) { return NULL; }

/* FILE 在 stdio.h里面定义. */
FILE __stdout;

/* MDK下需要重定义fputc函数, printf函数最终会通过调用fputc输出字符串到串口 */
int fputc(int ch, FILE *f) {
while ((USART1->SR & 0X40) == 0)
; /* 等待上一个字符发送完成 */

USART1->DR = (uint8_t)ch; /* 将要发送的字符 ch 写入到DR寄存器 */
return ch;
}
#endif
/* USER CODE END 0 */

LED

宏定义

1
2
3
4
5
6
7
8
9
10
/* gpio.h */
/* USER CODE BEGIN Private defines */
// Some Macros for LED control
#define LED_G_ON() HAL_GPIO_WritePin(LED_G_GPIO_Port, LED_G_Pin, GPIO_PIN_RESET)
#define LED_G_OFF() HAL_GPIO_WritePin(LED_G_GPIO_Port, LED_G_Pin, GPIO_PIN_SET)
#define LED_R_ON() HAL_GPIO_WritePin(LED_R_GPIO_Port, LED_R_Pin, GPIO_PIN_RESET)
#define LED_R_OFF() HAL_GPIO_WritePin(LED_R_GPIO_Port, LED_R_Pin, GPIO_PIN_SET)
#define LED_G_TOGGLE() HAL_GPIO_TogglePin(LED_G_GPIO_Port, LED_G_Pin)
#define LED_R_TOGGLE() HAL_GPIO_TogglePin(LED_R_GPIO_Port, LED_R_Pin)
/* USER CODE END Private defines */

LED 通信提示

image-20231201115920457

SCH

串口通信

  • 串口通讯方式:阻塞模式、中断模式、DMA 模式
1
2
3
4
5
6
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);
  • USART/UART

    • UART: 通用异步收发传输器(Universal Asynchronous Receiver/Transmitter)
    • USART: 通用同步/异步串行接收/发送器(Universal Synchronous/Asynchronous Receiver/Transmitter)

串口发送

img

:key: 相关定义

img

普通发送模式

1
HAL_UART_Transmit( &huart6 , (uint8_t *)"hello DISCO\r\n" , sizeof("hello DISCO\r\n"), 0xFFFF);

自定义 printf 函数

1
2
3
4
5
6
7
8
/*  used for a string send by usart */
void My_String_Printf( uint8_t* String )
{
while( *String != '\0' )
{
HAL_UART_Transmit( &huart6 , (uint8_t *)(String++), 1, 0xFFFF);
}
}
  • 使用

    • ```c
      My_String_Printf(“hello DISCO\r\n”);
      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

      #### 使用标准 printf

      ```c
      /* USER CODE BEGIN 0 */
      #include "stdio.h"
      /* 加入以下代码, 支持printf函数, 而不需要选择use MicroLIB */
      #if 1

      #if (__ARMCC_VERSION >= 6010050) /* 使用AC6编译器时 */
      __asm(".global __use_no_semihosting\n\t"); /* 声明不使用半主机模式 */
      __asm(
      ".global __ARM_use_no_argv \n\t"); /* AC6下需要声明main函数为无参数格式,否则部分例程可能出现半主机模式
      */

      #else
      /* 使用AC5编译器时, 要在这里定义__FILE 和 不使用半主机模式 */
      #pragma import(__use_no_semihosting)

      struct __FILE {
      int handle;
      /* Whatever you require here. If the only file you are using is */
      /* standard output using printf() for debugging, no file handling */
      /* is required. */
      };

      #endif

      /* 不使用半主机模式,至少需要重定义_ttywrch\_sys_exit\_sys_command_string函数,以同时兼容AC6和AC5模式
      */
      int _ttywrch(int ch) {
      ch = ch;
      return ch;
      }

      /* 定义_sys_exit()以避免使用半主机模式 */
      void _sys_exit(int x) { x = x; }

      char *_sys_command_string(char *cmd, int len) { return NULL; }

      /* FILE 在 stdio.h里面定义. */
      FILE __stdout;

      /* MDK下需要重定义fputc函数, printf函数最终会通过调用fputc输出字符串到串口 */
      int fputc(int ch, FILE *f) {
      while ((USART1->SR & 0X40) == 0); /* 等待上一个字符发送完成 */

      USART1->DR = (uint8_t)ch; /* 将要发送的字符 ch 写入到DR寄存器 */
      return ch;
      }
      #endif
      /* USER CODE END 0 */
  • 半主机模式与 C 库
    • 在嵌入式系统中,通过串口打印 log 是非常重要的调试手段,但是直接调用底层驱动打印信息非常不方便,在 c 语言中一般使用 printf 打印基本的显示信息,而默认 printf 的结果不会通过串口发送,解决方法主要有两种:
  1. 半主机模式:半主机是用于 ARM 目标的一种机制,可将来自应用程序代码的输入/输出请求传送至运行调试器的主机。 例如,使用此机制可以启用 C 库中的函数,如 printf() 和 scanf(),来使用主机的屏幕和键盘,而不是在目标系统上配备屏幕和键盘。
    • 简单的来说,半主机模式就是通过仿真器实现开发板在电脑上的输入和输出。和半主机模式功能相同的是 ITM 调试机制。ITM 是 ARM 在推出 semihosting 之后推出的新一代调试机制。这两种机制的运行均需要仿真器,否则无法运行。
  2. 微库:使用微库的话,不会使用半主机模式.
    • microlib 是缺省 C 库的备选库。 它用于必须在极少量内存环境下运行的深层嵌入式应用程序。 这些应用程序不在操作系统中运行
  • 避免使用半主机模式: 两种方法:微库法、代码法

    • 半主机模式简介(不使用)

      • 用于 ARM 目标的一种机制,可将来自应用程序代码的输入/输出请求传送至运行调试器的主机 ==> 简单说:就是通过仿真器实现开发板在电脑上的输入和输出
    • 方法一:微库法

      • 在魔术棒 Target 选项卡,勾选:Use Micro LIB,即可避免半主机模式

        image-20230331180338314

    • 方法二:==代码法==(usart.c ==> L34-88)

      • 1 个预处理、 2 个定义、3 个函数

      • 1,#pragma import(__use_no_semihosting),确保不从 C 库中使用半主机函数

        2,定义:__FILE 结构体,避免 HAL 库某些情况下报错

        3,定义: FILE __stdout,避免编译报错

        4,实现:_ttywrch、_sys_exit 和_sys_command_string 等三个函数

    • 对比

      | 方法 | 优点 | 缺点 |
      | ————— | ——————————————- | ————————————————- |
      | 微库法 | 简单 | 某些标准 C 库函数运行慢、兼容性差 |
      | ==代码法== | 标准 C 库函数运行快、兼容性好 | 稍微复杂 |

  • 实现 fputc 函数: 实现单个字符输出

image-20230331180431263

DMA 发送模式

:x: CubeMX 生成的 MX_DMA_Init(); 位置错误,需要修正

main.c

错误

1
2
3
4
5
// 正确顺序
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_DMA_Init();
MX_USART1_UART_Init();
  • CUBEMX 在 ADC 的初始化过程中加入了 DMA 和 ADC 之间产生关联的代码

  • 而 DMA 的时钟却在执行完 ADC 初始化后才开启,换句话就是如果先运行 ADC 初始化而没有开启 DMA 时钟会导致 DMA 配置失败,而不会返回任何错误代码

  • 配置

    • image-20231128205302273image-20231128205416139

    • NVIC

      • image-20231128205537083
      • 模式选择

        • Normal 正常模式,DMA 发送一次就停止发送

        • Circular 循环模式,会一直发送数据

          • 完成一次传输后会自动重新开始下一次传输,形成一个循环。这种模式适用于需要连续、循环传输数据的场景(音频、视频流等连续数据流的传输)
      • DMA 传输通道设置

        • DMA1 : DMA1 Channel 0~DMA1 Channel 7
        • DMA2: DMA2 Channel 1~DMA1 Channel 5
      • Dirction : DMA 传输方向

        • 外设到内存 Peripheral To Memory
        • 内存到外设 Memory To Peripheral
        • 内存到内存 Memory To Memory
        • 外设到外设 Peripheral To Peripheral
      • Priority: 传输速度

        • 最高优先级 Very Hight
        • 高优先级 Hight
        • 中等优先级 Medium
        • 低优先级;Low

串口接收

轮询接收模式

1
2
if( HAL_OK == HAL_UART_Receive( &huart6, &pData, 1, 1) )
printf("Receive Data:%c\r\n",pData);
  • 轮询的方法非常占用 CPU,一般不使用

中断接收模式

流程
  • HAL_UART_IRQHandler-此函数是 request(响应中断),即 UART 的 RX 中断入口。当有数据发送时,就会进入到这个函数中
    • image-20231129161443875
    • 正常情况下(即 errorflags = RESET)将会调用”UART_Receive_IT“进入处理数据的部分
  • UART_Receive_IT-可以理解为 RX 接收数据处理的函数
    • image-20231129161114451
    • 进入到这个函数的时候,会判断当前 RX 接收状态(重要)
    • 判断条件”—huart->RxXferCount”就是判断进来比特数是否到达指定的大小可以看到当满足条件是,将会关闭中断响应,同时调用”HAL_UART_RxCpltCallback“函数
      • 同时会关闭某些中断以及设置两个状态
        • image-20231129161407849
  • HAL_UART_RxCpltCallback-在完成 RX 数据接收后自行决定怎么操作的函数
    • image-20231129161606905
    • “\_weak”描述就是弱定义,指当用户在进行定义时,可以理解为这个定义就失效了_
  • HAL_UART_Receive_IT-重新开启 UART 的 Receive 中断
    • image-20231129161758741
      -
单字符

多字符状态下可能会丢失数据,甚至可能造成程序卡顿

  • 打开串口全局中断
    • image-20231128214446391
1
2
3
4
5
6
7
8
9
10
11
// usart.c
/* USER CODE BEGIN 1 */
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
if (huart->Instance == USART1) {
HAL_UART_Transmit_DMA(&huart1, (uint8_t *)"hello DISCO by DMA\r\n",
sizeof("hello DISCO by DMA\r\n"));
LED_G_TOGGLE();
HAL_UART_Receive_IT(&huart1, &rx_data, 1);
}
}
/* USER CODE END 1 */
  • 使能接收中断

    • 可以放在串口初始化阶段、主函数初始化阶段等位置

      • ```c
        HAL_UART_Receive_IT(&huart1, &rx_data, 1);
        1
        2
        3
        4
        5
        6
        7
        8
        9

        - <img src="C:/Users/bayyy/AppData/Roaming/Typora/typora-user-images/image-20231128214920020.png" alt="image-20231128214920020" style="zoom:50%;" />

        - 声明变量

        - 可以在 `usart.c` 中声明,头文件中声明外部变量

        - ```c
        extern uint8_t rx_data;
任意长度字符

:warning: 以 \r\n 结尾

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
73
74
75
76
// usart.c
uint8_t rx_buf[100];
uint16_t rx_sta = 0;
uint8_t rx_data[1];

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
if (huart->Instance == USART1) {
if ((rx_sta & 0x8000) == 0) /* 接收未完成 */
{
if (rx_sta & 0x4000) /* 接收到了0x0d(即回车键) */
{
if (rx_data[0] != 0x0a) /* 接收到的不是0x0a(即不是换行键) */
{
rx_sta = 0; /* 接收错误,重新开始 */
} else /* 接收到的是0x0a(即换行键) */
{
rx_sta |= 0x8000; /* 接收完成了 */
}
} else /* 还没收到0X0d(即回车键) */
{
if (rx_data[0] == 0x0d)
rx_sta |= 0x4000;
else {
rx_buf[rx_sta & 0X3FFF] = rx_data[0];
rx_sta++;

if (rx_sta > (100 - 1)) {
rx_sta = 0; /* 接收数据错误,重新开始接收 */
}
}
}
}
if (rx_sta & 0x8000) /* 接收完成 */
{
printf("%s\r\n", rx_buf);
printf("len: %d\r\n", rx_sta & 0x3FFF);
memset(rx_buf, 0, sizeof(rx_buf));
rx_sta = 0;
}
}
}


// stm32f1xx_it.c
/**
* @brief This function handles USART1 global interrupt.
*/
void USART1_IRQHandler(void) {
/* USER CODE BEGIN USART1_IRQn 0 */
int timeout = 0;
// #if SYS_SUPPORT_OS /* 使用OS */
// OSIntEnter();
// #endif

/* USER CODE END USART1_IRQn 0 */
HAL_UART_IRQHandler(&huart1);
/* USER CODE BEGIN USART1_IRQn 1 */
timeout = 0;
while (HAL_UART_GetState(&huart1) != HAL_UART_STATE_READY) {
timeout++;
if (timeout > HAL_MAX_DELAY) {
break;
}
}
timeout = 0;
while (HAL_UART_Receive_IT(&huart1, (uint8_t*)rx_data, 1) != HAL_OK) {
timeout++;
if (timeout > HAL_MAX_DELAY) {
break;
}
}
// #if SYS_SUPPORT_OS /* 使用OS */
// OSIntExit();
// #endif
/* USER CODE END USART1_IRQn 1 */
}

USART_RX_STA 剖析

在这里插入图片描述

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
#define USART1_REC_LEN 200
u8 USART1_RX_BUF[USART1_REC_LEN]; //接收缓冲,最大USART_REC_LEN个字节.
// 接收状态
// bit15, 接收完成标志
// bit14, 接收到0x0d
// bit13~0, 接收到的有效字节数目
u16 USART1_RX_STA=0; //接收状态标记

void USART1_IRQHandler(void)
{ u8 Res;
if((USART_GetITStatus(USART1,USART_IT_RXNE))!=RESET) //判断串口是否接收到数据
{
Res = USART_ReceiveData(USART1);
printf("%c",Res); //接收到则打印回电脑端
}
if((USART1_RX_STA &0X8000)==0) //接收未完成
{
if(USART1_RX_STA & 0X4000) //接收到回车0x0d ,执行
{
if(Res!=0x0a) USART1_RX_STA=0;
else USART1_RX_STA |=0X8000;//完成数据接收,把USART1_RX_STA高bit[15]置1
}
else //没有接收到回车
{
if(Res==0x0d) //接收到回车(0x0d)
USART1_RX_STA |=0X4000; //则把USART1_RX_STA高bit[14]置1
else { //
USART1_RX_BUF[USART1_RX_STA&0X3FFF]=Res;
USART1_RX_STA++;
if(USART1_RX_STA>(USART1_REC_LEN-1))
USART1_RX_STA = 0; //加到最大长度,会把标记清0
}
}
}
}
空闲中断
1
2
3
4
5
6
7
8
9
10
11
12
13
uint_8 rx_data[RX_BUF_MAX]

// 初始化
HAL_UARTEx_ReceiveToIdle_IT(&huart1, (uint8_t *)rx_data, RX_BUF_MAX);

void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size) {
if (huart->Instance == USART1) {
// 把收到的一包数据通过串口回传
HAL_UART_Transmit(&huart1, (uint8_t *)rx_data, Size, 0xff);
// 再次开启空闲中断接收,不然只会接收一次数据
HAL_UARTEx_ReceiveToIdle_IT(&huart1, rx_data, RX_BUF_MAX);
}
}

DMA 接收模式

  • DMA 方式直接开辟内存与外设之间的数据传输通道,可以减轻 CPU 的负担,增加数据处理速度
简介

重要函数简介

  • 发送函数 HAL_UART_Transmit_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)

    • image-20231129184540036
    • 以 DAM 模式发送 pData 指针指向的数据中固定长度的数据,并同时设置和使能 DMA 中断
    • 将 DMA 传输完成、半完成、错误的回调函数分别定向到了串口 DMA 传输完成、半完成、错误的回调函数UART_DMATransmitCplt、UART_DMATxHalfCplt、UART_DMAError
      • UART_DMATransmitCplt 最终调用了 __weak void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
  • 接收函数 HAL_StatusTypeDef HAL_UART_Receive_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)

    • 在 DMA 模式下接收大量数据,同时设置 DMA 线和哪个串口外设连接,以及将 DMA 线接收到的数据搬 *pData 对应地内存中,和上面 DMA 发送函数一样,此函数同时具有设置和使能 DMA 中断的功能
      • 接收完成后会调用串口接收完成回调函数 __weak void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
  • 停止函数 HAL_StatusTypeDef HAL_UART_DMAStop(UART_HandleTypeDef *huart)

    • 这个函数在 DMA 中断中要用到(包括发送和接收),调用这个函数,然后处理数据,之后再重新打开 DMA 发送与接收
      -
  • 查询剩余传输数据个数 __HAL_DMA_GET_COUNTER(__HANDLE__)

    • 这是一个宏定义,是查询 NDTR 寄存器的值,这个寄存器存储的是 DMA 传输的剩余传输数量。在接收数据时,用定义的最大传输数量减去这个剩余数量就是已经接收的数据个数。在发送数据时,用定义的最大传输数量减去这个剩余数量就是已经发送的数据个数。
1
#define __HAL_DMA_GET_COUNTER(__HANDLE__) ((__HANDLE__)->Instance->NDTR)
  • 清除空闲中断标志 __HAL_UART_CLEAR_IDLEFLAG(__HANDLE__)

    • 一个宏定义,在空闲中断中调用

  • 配置

  • image-20231128220055606

选择 Circular 模式,否则每次传输完成需要重新打开 DMA 接收 即调用 HAL_UART_Receive_DMA(&huart1, &pData, 1)

DMA 空闲模式
  • DMA mode 需要设置为 Normal 否则数据会循环填充,Size 所指示的数据长度会累计,直至填满
1
2
3
4
5
6
7
8
9
10
11
12
13
14
uint_8 rx_data[RX_BUF_MAX]

// 初始化
HAL_UARTEx_ReceiveToIdle_DMA(&huart1, (uint8_t *)rx_data, RX_BUF_MAX);

void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size) {
if (huart->Instance == USART1) {
// 把收到的一包数据通过串口回传
HAL_UART_Transmit(&huart1, (uint8_t *)rx_data, Size, 0xff);
// 再次开启空闲中断接收,不然只会接收一次数据
HAL_UARTEx_ReceiveToIdle_DMA(&huart1, rx_data, RX_BUF_MAX);
__HAL_DMA_DISABLE_IT(huart->hdmarx, DMA_IT_HT); // 关闭半传输中断
}
}

按钮

类型

  • 四端口按钮

img

  • 正点原子精英板

image-20231201124902529

轮询方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
uint8_t key_scan(void) {
if (HAL_GPIO_ReadPin(KEY_0_GPIO_Port, KEY_0_Pin) ==
GPIO_PIN_RESET) { // 按键按下 (低电平有效)
HAL_Delay(10); // 消抖
if (HAL_GPIO_ReadPin(KEY_0_GPIO_Port, KEY_0_Pin) == GPIO_PIN_RESET) {
while (HAL_GPIO_ReadPin(KEY_0_GPIO_Port, KEY_0_Pin) == GPIO_PIN_RESET) {
}; // 等待按键松开
return 1; // 按钮按下
}
}
return 0;
}

while (1) {
if (key_scan() != 0) {
printf("key pressed\r\n");
}
...
}

中断方式

  • 配置
    • image-20231201173832111
  • 打开中断
    • image-20231201173850014
1
2
3
4
5
6
/* USER CODE BEGIN 1 */
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) {
if (GPIO_Pin == GPIO_PIN_0) {
printf("key press!\r\n");
}
}

按键消抖

  • 常的按键所用开关为机械弹性开关,当机械触点断开、闭合时,由于机械触点的弹性作用,一个按键开关在闭合时不会马上稳定地接通,在断开时也不会一下子断开。因而在闭合及断开的瞬间均伴随有一连串的抖动,为了不产生这种现象而作的措施就是按键消抖。
  • 抖动时间的长短由按键的机械特性决定,一般为5ms ~ 10ms。这是一个很重要的时间参数,在很多场合都要用到。按键稳定闭合时间的长短则是由操作人员的按键动作决定的,一般为零点几秒至数秒。为确保 CPU 对键的一次闭合仅作一次处理,必须去除键抖动。在键闭合稳定时读取键的状态,并且必须判别到键释放稳定后再作处理

在这里插入图片描述

硬件消抖

适用于按键较少

  • RS 触发器

    • 在这里插入图片描述

    • 图中两个“与非”门构成一个 RS 触发器。当按键未按下时,输出为 0;当键按下时,输出为 1。此时即使用按键的机械性能,使按键因弹性抖动而产生瞬时断开(抖动跳开 B),只要按键不返回原始状态 A,双稳态电路的状态不改变,输出保持为 0,不会产生抖动的波形。也就是说,即使 B 点的电压波形是抖动的,但经双稳态电路之后,其输出为正规的矩形波。这一点通过分析 RS 触发器的工作过程很容易得到验证

  • 电容器

    • 在这里插入图片描述

    • 利用电容的放电延时,采用并联电容法,也可以实现硬件消抖。如图所示,由于电容两端电压不能突变,使得按键两端的电压平缓变化,直至电容充放电到达一定电压阈值时,单片机才读取到电平变化

软件(延时)消抖

  • 原理:检测出键闭合后执行一个延时程序,5ms ~ 10ms(取决于机械特性)的延时,让前沿抖动消失后再一次检测键的状态,如果仍保持闭合状态电平,则确认为真正有键按下。当检测到按键释放后,也要给 5ms ~ 10ms 的延时,待后沿抖动消失后才能转入该键的处理程序。
1
2
3
4
5
6
if (HAL_GPIO_ReadPin(KEY_GPIO_Port, KEY_Pin) == RESET) {  // 按键按下 (低电平有效)
HAL_Delay(10); // 消抖
if (HAL_GPIO_ReadPin(KEY_GPIO_Port, KEY_Pin) == GPIO_PIN_RESET) {
/* user code */
}
}

特点:简单方便;空跑浪费资源

:warning: 如果按键是用中断方式实现的,那就更不能在中断服务函数里面使用延时函数,因为==中断服务函数最基本的要求就是快进快出!==

软件(定时器)消抖

  • 原理:按键采用中断驱动方式,当按键按下以后触发按键中断,在按键中断中开启一个定时器,定时周期为 10ms,当定时时间到了以后就会触发定时器中断,最后在定时器中断处理函数中读取按键的值,如果按键值还是按下状态那就表示这是一次有效的按键。

在这里插入图片描述

特点:节约 CPU 资源;消耗一个定时器

  • 配置 这里使用通用定时器 TIME2

    • image-20231201160621324

    • image-20231201160633409

    • 外部时钟 -> APB1 -> 72MHz

  • 定时器计数时间的计算公式:Tout=(arr+1)*(psc+1)/Tclk,本例程中 APB1 外设时钟频率为 72MHZ,故 Tclk=1/72000000 所以 Tout=(3600*200)/72us =10ms

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
uint8_t key_sta = 0;  // 按键状态 (bit8: LED_0 按下, bit_7: LED_1 按下)

/* USER CODE BEGIN 1 */
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) {
if (GPIO_Pin == KEY_0_Pin) {
key_sta |= 0x80;
HAL_TIM_Base_Start_IT(&htim2);
} else if (GPIO_Pin == KEY_1_Pin) {
key_sta |= 0x40;
HAL_TIM_Base_Start_IT(&htim2);
}
}

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) {
if (htim->Instance == TIM2) {
HAL_TIM_Base_Stop_IT(&htim2);
if (key_sta & 0x80) {
if (HAL_GPIO_ReadPin(KEY_0_GPIO_Port, KEY_0_Pin) == GPIO_PIN_RESET) {
printf("key 0 pressed\r\n");
}
}
if (key_sta & 0x40) {
if (HAL_GPIO_ReadPin(KEY_1_GPIO_Port, KEY_1_Pin) == GPIO_PIN_RESET) {
printf("key 1 pressed\r\n");
}
}
key_sta = 0;
}
}
/* USER CODE END 1 */

定时器

STM32 定时器

image-20230416162450244

定时器类型 主要功能
基本定时器 没有输入输出通道,常用作时基,即定时功能(主要用于驱动 DAC)
通用定时器 具有多路独立通道,可用于输入捕获/输出比较,也可用作时基 (==通用==,定时计数、PWM 输出、输入捕获、输出比较)
高级定时器 除具备通用定时器所有功能外,还具备带死区控制的互补信号输出、刹车输入等功能(可用于电机控制、数字电源设计等)

高级定时器具有捕获/比较通道和互补输出,通用定时器只有捕获/比较通道,基本定时器没有以上两者

:key: STM32 的众多定时器中我们使用最多的是高级定时器和通用定时器,而高级定时器一般也是用作通用定时器的功能

image-20230416164423738

通用定时器

  • 位于低速的==APB1 总线==上(APB1)
  • 16 位向上、向下、向上/向下(中心对齐)计数模式,自动装载计数器(TIMx_CNT)
  • 16 位可编程(可以实时修改)预分频器(TIMx_PSC),计数器时钟频率的分频系数 为 1 ~ 65535 之间的任意数值
  • 4 个独立通道(TIMx_CH1~4)

  • 这些通道可以用来作为

    1. 输入捕获
    2. 输出比较
    3. PWM 生成(边缘或中间对齐模式)
    4. 单脉冲模式输出
  • 可使用外部信号(TIMx_ETR)控制定时器和定时器互连(可以用 1 个定时器控制另外一个定时器)的同步电路
  • 如下事件发生时产生中断/DMA(6 个独立的 IRQ/DMA 请求生成器)
    1. 更新:计数器向上溢出/向下溢出,计数器初始化(通过软件或者内部/外部触发)
    2. 触发事件(计数器启动、停止、初始化或者由内部/外部触发计数)
    3. 输入捕获
    4. 输出比较
    5. 支持针对定位的增量(正交)编码器和霍尔传感器电路
    6. 触发输入作为外部时钟或者按周期的电流管理
  • STM32 的通用定时器可以被用于:测量输入信号的脉冲长度(输入捕获)或者产生输出波形(输出比较和 PWM)等
  • 使用定时器预分频器和 RCC 时钟控制器预分频器,脉冲长度和波形周期可以在几个微秒到几个毫秒间调整。STM32 的每个通用定时器都是完全独立的,没有互相共享的任何资源。

计数器模式

计数器模式 溢出条件
递增计数模式 CNT==ARR
递减计数模式 CNT==0
中心对齐模式 CNT\=\=ARR-1、CNT\=\=1

image-20230416162811368

递增计数模式

PSC = 1 ARR = 36

image-20230416162926567

递减计数模式

PSC = 1 ARR = 36

image-20230416163313878

#

中心对齐计数模式

PSC = 0 ARR = 6

image-20230416163424336

USB

常见 USB 引脚定义

Type C --- 引脚图解_typec尾插针脚图解-CSDN博客

type-A(USB2.0 4 线)

imgimg

序号 符号 符号名称 功能说明
1 VCC 电源 电源输入
2 D- 数据传输端 - 传输数据
3 D+ 数据传输端 + 传输数据
4 GND 地线

type-B(USB2.0 4 线)

img

序号 符号 符号名称 功能说明
1 VCC 电源 电源输入
2 D- 数据传输端 - 传输数据
3 D+ 数据传输端 + 传输数据
4 GND 地线

type-A(USB3.0 9 线)

imgimg

引脚序号 符号 符号名称 功能
1 VBUS 电源 提供电源
2 D- 数据传输端 Data- 传输数据
3 D+ 数据传输端 Data+ 传输数据
4 GND 地线
5 STDA_SSRX- 超高速接收机差分对 RX- 传输数据
6 STDA_SSRX+ 超高速接收机差分对 RX+ 传输数据
7 GND_DRAIN 信号地
8 STDA_SSTX- 超高速发射机差分对 TX- 传输数据
9 STDA_SSTX+ 超高速发射机差分对 TX+ 传输数据

type-B(USB3.0 9 线)

imgimg

引脚序号 符号 符号名称 功能
1 VBUS 电源 提供电源
2 D- 数据传输端 Data- 传输数据
3 D+ 数据传输端 Data+ 传输数据
4 GND 地线
8 STDA_SSTX- 超高速发射机差分对 TX- 传输数据
9 STDA_SSTX+ 超高速发射机差分对 TX+ 传输数据
7 GND_DRAIN 信号地
8 STDA_SSRX- 超高速接收机差分对 RX- 传输数据
9 STDA_SSRX+ 超高速接收机差分对 RX+ 传输数据

Mini-A/B(USB2.0 5 线)

imgimage-20231203180034974

序号 符号 符号名称 功能说明
1 VCC 电源 电源输入
2 D- 数据传输端 - 传输数据
3 D+ 数据传输端 + 传输数据
4 ID (不知道) OTG 功能中使用
5 GND 地线

Micro-A/B(USB2.0 5 线)

imgimg

序号 符号 符号名称 功能说明
1 VCC 电源 电源输入
2 D- 数据传输端 - 传输数据
3 D+ 数据传输端 + 传输数据
4 ID (不知道) OTG 功能中使用
5 GND 地线

Micro-B(USB3.0 10 线)

imgimg

引脚序号 符号 符号名称 功能
1 VBUS 电源 提供电源
2 D- 数据传输端 Data- 传输数据
3 D+ 数据传输端 Data+ 传输数据
4 ID 自己查
5 GND 地线
6 STDA_SSTX- 超高速发射机差分对 TX- 传输数据
7 STDA_SSTX+ 超高速发射机差分对 TX+ 传输数据
8 GND_DRAIN 信号地
9 STDA_SSRX- 超高速接收机差分对 RX- 传输数据
10 STDA_SSRX+ 超高速接收机差分对 RX+ 传输数据

type-C

:key: 分为 6pin(充电)、12pin、16pin、24pin(全功能)

imgimg

img

引脚 描述 引脚 描述
A1 GND 接地 B12 GND 接地
A2 SSTXp1 SuperSpeed 差分信号#1,TX,正 B11 SSRXp1 SuperSpeed 差分信号#1,RX,正
A3 SSTXn1 SuperSpeed 差分信号#1,TX,负 B10 SSRXn1 SuperSpeed 差分信号#1,RX,负
A4 VBUS 总线电源 B9 VBUS 总线电源
A5 CC1 Configuration channel B8 SBU2 Sideband use (SBU)
A6 Dp1 USB 2.0 差分信号,position 1,正 B7 Dn2 USB 2.0 差分信号,position 2,负
A7 Dn1 USB 2.0 差分信号,position 1,负 B6 Dp2 USB 2.0 差分信号,position 2,正
A8 SBU1 Sideband use (SBU) B5 CC2 Configuration channel
A9 VBUS 总线电源 B4 VBUS 总线电源
A10 SSRXn2 SuperSpeed 差分信号#2,RX,负 B3 SSTXn2 SuperSpeed 差分信号#2,TX,负
A11 SSRXp2 SuperSpeed 差分信号#2,RX,正 B2 SSTXp2 SuperSpeed 差分信号#2,TX,正
A12 GND 接地 B1 GND 接地

CH3240

版本区别

  • CH340 是沁恒微电子于 2004 年推出的一颗经典 USB 转串口芯片

  • CH340:USB 转串口经典型号,拥有 SOP8/MSOP10/ESSOP10/SOP16 等封装,无需外部晶振

  • CH341:USB 转串口/IIC/SPI/并口/打印口 等
  • CH342:USB 转双串口,支持自定义 IO 口电压,支持 VCP/CDC 模式
  • CH343:USB 转高速串口(6Mbps),支持自定义 IO 口电压,防串口倒灌电,支持 VCP/CDC 模式

在这里插入图片描述

USB转串口芯片 CH340 - 南京沁恒微电子股份有限公司

点击查看大图