Skip to content

五、LED模块

c
void led_disp(uint8_t *ucLed)
{
	//用于记录当前LED状态的临时变量
	uint8_t temp = 0x00;
	//用于记录之前LED状态的变量,用于判断是否需要更新显示
	static uint8_t temp_old = 0xff;
	
	for(int i = 0; i<8; i++)
	{
		//将当前元素左移相应的位数,并累加到temp中
		temp |= (ucLed[i] << (7 - i));
	}
	
	//仅当前状态与之前状态不同的时候,才更新显示
	if(temp != temp_old)
	{
		//将GPIOC低字节清零,高字节更新为新的temp值
		GPIOC -> ODR &= 0x00ff;	//清楚GPIOC高字节
		GPIOC -> ODR |= ~(temp << 8);	//设置GPIO高字节为temp的相反值
		GPIOD -> BSRR |= 0x01 << 2;	//设置GPIOD第2位
		GPIOD -> BRR |= 0x01 << 2;	//重置GPIOD第2位
		temp_old = temp;	//更新记录的旧状态
	}
}
c
void led_proc(void)
{
    //显示当前Led_Pos位置的LED灯状态
    led_disp(ucLed);
}

一、使用ODR、BSRR和BRR来设置GPIO引脚的状态各有不同的应用场景和优缺点

1.ODR(OutPut Data Register)

功能:直接设置GPIO引脚的输出状态。ODR控制所有引脚的输出电平状态,使用位操作可以同时设置多个引脚的状态。

优点:可以一次性设置多个引脚的状态。

缺点:可能会影响其他引脚的状态,因为它直接覆盖寄存器的所有位。更改时需要确保不会不小心更改到其他引脚的状态。

示例:

c
GPIOD ->ODR = (1 << 2); //将第2个引脚设置为高电平,其余引脚保持原状

2.BSRR(Bit Set/Reset Register)

功能:用于设置或重置特定的GPIO引脚。BSRR中的高位用于设置引脚为高电平,低位用于将引脚设置为低电平。

优点:只改变指定引脚的状态,而不影响其他引脚。适合需要频繁更改某些引脚的状态时使用。

缺点:需要额外的位操作,通常在操作多个引脚是不如ODR方便。

示例:

c
GPIOD -> BSRR = (1 << 2); //将第2个引脚设置为高电平
GPIOD -> BSRR = (1 << (2 + 16)); //将第2个引脚设置为低电平

3.BRR(Bit Reset Register)

功能:用于将GPIO引脚设置为低电平。BRR的作用是将指定引脚设置为低电平。

优点:直接和简单地将引脚设置为低电平,操作明确。

缺点:仅能设置为低电平,无法设置为高电平。如果需要设置为高电平,需要使用BSRR。

示例:

c
GPIO -> BRR =1 << 2); //将第2个引脚设置为低电平

总结

· ODR : 适合同时设置多个引脚状态,但要小心它会覆盖寄存器的其他位。

· BSRR : 适合需要单独控制引脚状态时使用,能够单独设置高电平或低电平。

· BRR : 仅用于将引脚设置为低电平,如果需要设置为高电平,通常或与BSRR配合使用。

选择使用哪个寄存器取决于你的具体需求,例如是否需要同时操作多个引脚, 或是否需要频繁更改单个引脚的状态。

1.上拉(Pull-up)

· 功能:上拉电阻将引脚在没有外部信号时拉高到逻辑高电平(通常是VCC电平)。

· 作用:防止引脚悬空导致不稳定状态。如果引脚未连接任何信号源或是开路的情况下,电阻将引脚的电平拉到高电平。

· 典型应用:常用于需要稳定高电平的数字输入信号。

工作原理

· 内部有一个电阻连接到VCC(通常是3.3V或5V),当引脚未被驱动时,电阻将引脚拉至高电平。

2.下拉(Pull-down)

· 功能:下拉电阻将引脚在没有外部信号是拉低到逻辑低电平(通常是地电平GNG)。

· 作用:防止引脚悬空导致不稳定状态。如果引脚未连接任何信号源或是开路的情况下,电阻将引脚的电平拉到低电平。

·典型应用:常用于需要稳定低电平的数字输入信号。

工作原理

· 内部有一个电阻连接到地(GND),当引脚未被驱动时,电阻将引脚拉至低电平。

3.浮空(Floating)

· 功能:浮空状态下,引脚没有链接到上拉或下拉电阻,因此引脚的电平是未确定的(悬空)。

· 作用:引脚的电平可以是高电平或低电平,取决于外部电路或信号源的状态。引脚处于悬空状态,容易受到噪声干扰,可能导致不稳定的电平。

· 典型应用:当引脚的状态由外部电路控制,并且需要引脚处于高阻抗状态时使用。

工作原理

· 引脚没有内部电路与VCC或GND连接,完全依赖外部电路的输入信号。

总结

· 上拉(Pull-up):提供一个向高电平的电流路径,确保引脚在未驱动时为高电平。

· 下拉(Pull-down):提供一个向低电平的电流路径,确保引脚在未驱动时为低电平。

· 浮空(Floating):引脚没有上拉或下拉电阻,状态不确定,容易受到噪声影响。

六、Key模块

底层代码(四行代码版本)

c
#include key_app.h
uint8_t key_val = 0;	//当前按键状态
uint8_t key_old = 0;	//前一按键状态
uint8_t key_down = 0;	//按下的按键
uint8_t key_up = 0;		//释放的按键

/**
 * @brief 读取按键状态
 *
 * 该函数读取连接在GPIO引脚上的按键状态,并返回相应的按键编号.
 *
 * @return 返回按键编号.0表示没有按键按下,1-4表示对应的按键被按下.
 */
uint8_t key_read(void)
{
    //用于存储按键状态的临时变量
    uint8_t temp = 0;
    
    //检查GPIOB引脚0的状态
    if(HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_0) == GPIO_PIN_RESET)
        temp = 1;	//如果引脚状态为RESET,则按键1按下
    //检查GPIOB引脚1的状态
    if(HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_1) == GPIO_PIN_RESET)
        temp = 2;	//如果引脚状态为RESET,则按键2按下
    //检查GPIOB引脚2的状态
    if(HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_2) == GPIO_PIN_RESET)
        temp = 3;	//如果引脚状态为RESET,则按键3按下
    //检查GPIOA引脚0的状态
    if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET)
        temp = 4;	//如果引脚状态为RESET,则按键4按下
    
    //返回检测到的按键编号
    return temp;
}

/**
 * @brief 按键处理函数
 *
 * 该函数用于扫描按键的状态,并更新按键按下和释放的标志
 */
void key_proc(void)
{
    //读取当前按键状态
    key_val = key_read();
    //计算按下的按键(当前按下状态与前一状态异或,并与当前状态相与)
    key_down = key_val & (key_old ^ key_val);
    //计算释放的按键(当前未按下状态与前一状态异或,并与前一状态相与)
    key_up = ~key_val & (key_old ^ key_val);
    //更新前一按键状态
    key_old = key_val;
    
}

底层代码(状态机版本)

什么是状态机?

状态机是一种编程模式,用来管理和处理对象或系统的各种状态,以及在这些状态之间转换。

举个例子

你的手机的闹钟应用由三个状态:

1.未设定:闹钟没有被设定。

2.已设定:闹钟已经设定好,但还没到时间。

3.响铃中:到了设定的时间,闹钟开始响铃。

状态机的工作原理

1.状态:一个系统当前的情况,比如”未设定“、”已设定“或”响铃中“。

2.时间:某些触发条件,比如”设定闹钟“、”时间到“或”关闭闹钟“

3.转换:从一个状态到另一个状态的过程,比如”设定闹钟“把状态从”未设定“转换到”已设定“。

为什么要用状态机?

状态机可以帮助我们清晰地描述和管理系统的各种状态和状态之间的切换。这在处理复杂逻辑、按键输入、用户界面等方面特别有用。

按键状态机的例子

· 状态1:按键没有被按下。

· 状态2:按键被按下。

· 状态3:按键被松开。

按键的状态会根据你是否按下或松开按键而发生变化。使用状态及可以让我们很清晰的知道当前按键的状态是什么,并且可以很方便地处理按键按下和松开时的逻辑。

总结

状态机就是一种管理不同状态及其转换方法,类似于管理闹钟的状态。通过状态机,我们可以清楚地处理复杂的逻辑和条件变化。

c
typedef struct
{
  GPIO_TypeDef *gpiox;	//指向GPIO端口的指针,例如GPIOA、GPIOB等。
  uint16_t pin;	//指定GPIO引脚,例如GPIO_PIN_0、GPIO_PIN_1等。
  uint16_t ticks;	//用于计时的变量,通常用于去抖处理。
  uint8_t level;	//当前按键的电平状态,高电平或低电平。
  uint8_t id;	//按键的唯一标识符,可以用于区分不同的按键。
  uint8_t state;	//按键的当前状态,可能用于表示按键是否被按下或释放。
  uint8_t debouce_cnt;	//按键去抖动计数器,用于防止按键抖动引起的误触发。
  uint8_t repeat;	//按键重复按下的次数。
}button;
c
button btns[4];	//按键数组

//按键初始化函数
void key_init(void)
{
  //初始化第一个按键
  btns[0].gpiox = GPIOB;	//指定GPIO端口
  btns[0].pin = GPIO_PIN_0;	//指定引脚
  btns[0].level = 1;	//设置初始电平
  btns[0].id = 0;	//设置按键ID

  //初始化第一个按键
  btns[0].gpiox = GPIOB;	//指定GPIO端口
  btns[0].pin = GPIO_PIN_1;	//指定引脚
  btns[0].level = 1;	//设置初始电平
  btns[0].id = 1;	//设置按键ID

  //初始化第一个按键
  btns[0].gpiox = GPIOB;	//指定GPIO端口
  btns[0].pin = GPIO_PIN_2;	//指定引脚
  btns[0].level = 1;	//设置初始电平
  btns[0].id = 2;	//设置按键ID

  //初始化第一个按键
  btns[0].gpiox = GPIOA;	//指定GPIO端口
  btns[0].pin = GPIO_PIN_0;	//指定引脚
  btns[0].level = 1;	//设置初始电平
  btns[0].id = 3;	//设置按键ID
}
c
//按键任务处理函数
void key_task(button *btn)
{
  //读取按键当前电平
  uint8_t gpio_level = HAL_GPIO_ReadPin(btn -> gpiox,btn -> pin);
  
  //如果按键状态大于0,则递增计时器
  if(btn -> state > 0)
    btn -> ticks++;
  
  //如果当前电平与按键记录的电平不同,进行去抖动处理
  if(btn -> level != gpio_level)
  {
    //计数达到3次,确认电平变化
    if(++(btn -> debouce_cnt) >= 3)
    {
      btn -> level = gpio_level;	//更新电平
      btn -> debouce_cnt = 0;	//重置去抖动计数器
    }
  }
  else
  {
    btn -> debouce_cnt = 0;	//电平没有变化,重置去抖动计数器
  }
  
  //按键状态机
  switch(btn -> state)
  {
      case 0:	//初始状态
      if(btn -> level == 0)	//按键按下
      {
        btn -> ticks = 0;	//重置计数器
        btn -> repeat = 1;	//初始按键重复计数
        btn -> state = 1; 	//进入按键按下状态
      }
      break;
    case 1:	//按键按下状态
      if(btn -> level != 0)	//按键松开
      {
        if(btn -> ticks >= 15)	//按键长按
        {
          btn -> repeat = 0;	//防止释放的时候再次触发单机事件
        }
        btn -> ticks = 0;	//重置计数器
        btn -> state = 2;	//进入按键释放状态
      }
      break;
    case 2:	//按键释放状态
      if(btn -> ticks >= 15)	//计时器达到阈值	
      {
        btn -> state = 0;	//返回初始状态
        
        
        
      }
      else
      {
        if(btn -> level == 0)	//按键再次按下
        {
          btn -> repeat++;	//递增重复计数
          btn -> ticks = 0;	//重置计数器
          btn -> state = 1;	//返回按键按下状态
        }
      }
      break;
  }
}

//按键状态处理函数
void key_state(void)
{
  for(uint8_t i = 0; i<4; i++)	//遍历所有按键
  {
    key_task(&btns[i]);	//处理每个按键的状态
  }
  
  
  
}

七、LCD模块

· 尺寸:2.4寸

· 类型:MCU屏幕

· 分辨率:240 * 320

· 接口:8080并行时序

LED与LCD引脚冲突问题及解决方案

问题描述:

由于LCD和LED公用部分引脚,可能会导致信号冲突问题。然而,由于系统中存在锁存器,LCD和LED的信号不会直接相互影响。

解决方案:

1.锁存器的作用:

· 锁存器存在使得LCD输出信号的改变不会引起LED灯的变化。

· 只有在给锁存器发送更新信号后,LED的状态才会改变。

2.引脚更改的影响:

· 如果更改了LED的引脚,不会引起LCD的变化。

· LCD接口使用的是8080时序,更改引脚满足其时序要求的几率非常小。

3.8080时序:8080时序是一种严格的数据传输协议,有一系列规定的步骤和时间要求。

LCD显示屏:

·数据线(GPIOC0 - GPIOC15):这16根线就像16条车道,用来传输需要在屏幕上显示的图像或文字的数据。

· 片选信号(PB9):就像一个告诉公路的入口大门,当片选信号为低时,大门打开,数据可以进入LCD;为高时,大门关闭,数据不能进入。

· 命令/数据选择信号(PB8):像一个路牌,只是车辆(数据)是否时普通车(数据)还是特殊车(命令)。PB8为低时,表示是命令;为高时,表示是数据。

· 写使能信号(PB5):类似一个写入信号,当PB5为低时,允许数据写入LCD。

· 读使能信号(PA8):类似一个读取信号,当PA8为低时,允许从LCD读取数据。

信号操作逻辑

1.写命令:

· 将PB8(RS)设置为低电平(LCD_RS_CLR),表示传输命令。

· 通过GPIOC传输命令数据。

· 将PB5(WR)拉低(写使能)。

· 拉高PB5(WR),完成写入操作。

2.写数据

· 将PB8(RS)设置为高电平(LCD_RS_SET),表示传输数据。

· 通过GPIOC传输数据。

· 将PB5(WR)拉低(写使能)。

· 拉高PB5(WR),完成写入操作。

3.读数据

· 将PB8(RS)设置为高电平(LCD_RS_SET),表示传输数据。

· 将PA8(RD)拉低(读使能)。

· 从GPIOC读取数据。

· 拉高PA8(RD),完成读取操作。

底层代码

c
#include "lcd.h"
#include "stdio.h"
#include "stdarg.h"

/**
 * @brief	格式化字符串并显示在指定的LCD行上
 *
 * 该函数接受一个行号和一个格式化字符串(类似于printf)
 * 格式化字符串后,将其显示在LCD的指定行上
 *
 * @param	Line	要显示字符串的LCD行号
 * @param format	格式化字符串,后跟要格式化的参数
 *
 * 该函数内部使用‘vsprintf’来格式化字符串,然后
 * 调用‘LCD_DisplayStringLine’在LCD上显示格式化后的字符串
 *
 * 示例用法:
 * @code
 * LCDSprintf(0, "Temperature: %d C", temperature);
 * @endcode
 */
void LcdSprintf(uint8_t Line, char *format, ...)
{
  char String[21];	//缓冲区用于存储格式化后的字符串
  va_list arg;	//参数列表用于存储可变参数
  va_start(arg, format);	//使用格式化字符串初始化参数列表
  vs_printf(String, format, arg);	//格式化字符串并存储在缓冲区中
  va_end(arg);	//清理参数列表
  LCD_DisplayStringLine(Line, (u8 *)String);	//在LCD指定行显示格式化后的字符串
}

void lcd_proc(void)
{
  
}

1.va_list、va_start和va_end

这些是处理可变参数函数的标准宏和类型,定义在头文件<stdarg.h>中。

· va_list:

·va_list是一个类型,用于声明变量,该变量将存储传递给可变参数函数的参数列表。

· va_start:

· va_start宏初始化va_list变量,使其指向可变参数列表中的第一个参数。

· 宏原型:void va_start(va_list ap, last);

·参数:

·ap:va_list类型变量。
·last:参数列表中最后一个固定参数,即可变参数列表之前的参数。

· va_end:

#### 	· va_end宏用于结束va_list变量的处理。

#### 	· 宏原型:void va_end(va_list ap);

· 参数:

· ap:va_list类型变量。

2.vsprintf

· vsprintf是一个标准库函数,定义在头文件<stdio.h>中。

· 功能:将格式化的数据写入字符串中,类似于sprintf,但接受一个va_list类型的参数列表。

· 函数原型:int vsprintf(char *str, const char *format, va_list ap);

· 参数:

​ str:指向存储格式化字符串的缓冲区。

​ format:格式化字符串。

​ ap:va_list类型变量,表示可变参数列表。

返回值:返回写入str的字符数(不包括终止符)。

3.LCD_DisplayStringLine:

​ LCD_DisplayStringLine是一个用户自定义函数,用于在LCD的指定行上显示字符串。

​ 函数原型:void LCD_DisplaySpringLine(u8 Line, u8 *ptr);

​ 参数:

​ Line:指定要显示字符串的LCD行号。

​ ptr:指向要显示的字符串。

综合说明

LcdSprintf函数的工作流程如下:

1.声明一个字符数组String作为缓冲区,用于存储格式化后的字符串。

2.声明一个va_list类型的变量arg,用于处理可变参数。

3.使用va_start宏初始化arg,使其指向格式化字符串format之后的第一个可变参数。

4.使用vaprintf函数,将格式化字符串和可变参数列表格式化为最终的字符串,并存储在String缓冲区中。

5.使用va_end宏结束va_list变量的处理。

6.调用LCD_DisplaySpringLine函数,在LCD的指定行上显示格式化后的字符串。

1.LCD初始化:

​ LCD_Init:初始化控制器,选择适当的LCD控制器初始化函数。

2.颜色设置:

​ LCD_SetTextColor:设置文本颜色。

​ LCD_SetBackColor:设置背景颜色。

3.清屏和清行:

​ LCD_Clear:清除整个LCD屏幕。

​ LCD_ClearLine:清除指定的行。

八、Uart模块

1.UART参数设置

基本参数

· Baud Rate(波特率):配置UART的通信速率,以比特每秒(bps)为单位。常见值有9600、115200等。

· Word Length(字长):设置每个数据帧的位数。常见设置包括8位和9位。

· Parity(奇偶校验):配置奇偶校验。选项包括None(无)、Even(偶校验)和Odd(奇校验)。

· Stop Bits(停止位):设置每个数据帧的停止位数。选项包括1位、1.5位、2位。

· Mode(模式):配置UART的工作模式,包括Tx(发送)、Rx(接收)或TxRx(收发)。

· Hardware Flow Control(硬件流控制):配置硬件流控制选项。选项包括None(无)、RTS、CTS和RTS/CTS。

高级参数

· Over Sampling(过采样):配置过采样比例,常见值有16和8.

· One Bit Sampling(单比特采样):启用或禁用单比特采样模式。

· Auto BaudRate(自动波特率):启用或禁用自动波特率检测。

· Receiver Time Out(接收超时):配置接收超时功能,防止长时间等待数据。

· Lin Mode(LIN模式):启用或禁用LIN(本地互联网络)模式。

2.GPIO设置

在GPIO设置选项卡中,可以为UART配置相应的引脚:

· Pin Configuration(引脚配置):选择用于UART Tx和Rx的引脚,并配置其模式(如复用功能)。

· Pin Type(引脚类型):配置引脚的输入类型(如浮空输入、上拉输入等)。

3.NVIC设置

NVIC(嵌套向量中断控制器)设置用于配置UART中断优先级:

· Preemption Priority(抢占优先级):设置UART中断的抢占优先级。

· Sub Priority(子优先级):设置UART中断的子优先级。

4.DMA设置

DMA(直接内存访问)设置用于配置UART的DMA功能:

· DMA Requests(DMA请求):选择对应的UART DMA请求(如UART_TX、UART_RX)。

· DMA Channel(DMA通道):分配DMA通道用于UART数据传输。

· DMA Mode(DMA模式):配置DMA的工作模式,如正常模式、循环模式等。

底层代码(串口中断+超时解析)

c
#include "string.h"
typedef struct __FILE FILE;
int fputc(int ch, FILE *str)
{
  HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 10);
  return ch;
}

HAL_UART_Receive_IT(&huart1, uart_rx_buffer, 1);

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
  if(huart -> Instance == USART1)
  {
    uart_rx_ticks = uwTick;
    uart_rx_index++;
    HAL_UART_Receive_IT(&huart1, &uart_rx_buffer[uart_rx_index], 1);
  }
}

DMA传输模式

Normal:正常模式

· 描述:当一次DMA数据传输完后,停止DMA传输,也就是只传输一次。

Circular:循环模式

描述:传输完成后又重新开始继续传输,不断循环。

DMA指针递增设置

Increment Address:地址指针递增

  • Src Memory(外设地址寄存器):

    • 功能:设置传输数据时外设地址是否递增。如果设置为递增,那么下一次传输时地址增加一个Data Width(数据宽度)字节。
    • 例子:串口发送数据是将数据不断存进固定外设地址(串口的发送数据寄存器, USARTx_TDR),所以外设的地址是不递增。
  • Dst Memory(内存地址寄存器):

    • 功能:设置传输数据时内存地址是否递增。如果设置为递增,那么下一次传输时地址增加一个Data Width(数据宽度)字节。
    • 例子:内存储器存储的时要发送的数据,所以地址指针要递增,保证数据依次被发出。

总结

在DMA传输中,对于串口发送数据:

  • 外设地址(Src Memory)是不递增的,因为数据要不断存入固定的串口发送数据寄存器。
  • 内存地址(Dst Memory)是递增的,以确保要发送的数据依次从内存中取出并发送出去。

环形存储区(Ring Buffer)

环形缓存区,也叫环形缓冲区(Ring Buffer)或循环缓冲区,是一种数据结构。它的特点是缓存区的头和尾是连接在一起的,形成一个环。当数据写入缓冲区时,指针会不断前进,当到达缓冲区的末尾时,会重新回到开头。这样就实现了循环。

环形缓冲区的构成

  1. 缓冲区数组:存储数据的地方。
  2. 头指针(Head Pointer):指向要读取数据线的位置。
  3. 尾指针(Tail Pointer):指向要写入数据的位置。

举例解释

假设我们有一个大小为8的环形缓冲区,并且我们需要处理一系列数据的接收和发送。

  1. 初始状态:
    • 缓冲区:[ _, _ , _ , _ , _ , _ , _ , _ , _ ]
    • 头指针和尾指针都在位置0.
  2. 写入数据:
    • 假设我们写入数据1,2,3.
    • 缓冲区:[1, 2, 3, _ , _ , _ , _ , _ ]
    • 尾指针在位置3,头指针仍在位置1。
  3. 读取数据:
    • 读取两个数据:1和2.
    • 缓冲区:[ _ , _ , 3 , _ , _ , _ , _ , _ , _ ]
    • 头指针移动到位置2,尾指针在位置3。
  4. 继续写入数据:
    • 写入数据 4,5,6,7,8。
    • 缓冲区:[4 , 5 , 6 , 7 , 8 , _ , _ , _ ]
    • 尾指针回到位置0(因为环形缓冲区已经到达末尾,继续从开头写入),头指针在位置2。

环形缓冲区的优缺点

优点

  1. 高效利用空间:因为是循环的,不会出现未使用的空间。
  2. 避免数据溢出:由于环形缓冲区大小固定,写入数据前可以判断是否有空间。
  3. 实时性强:适用于需要实时处理数据的应用,如串口通信。

缺点

  1. 管理复杂:需要维护头指针和尾指针的正确位置,避免数据覆盖。
  2. 空间固定:环形缓冲区的大小是固定的,如果数据量超过缓冲区大小,就需要额外处理。

对比普通串口接收的优劣势

普通串口接收

在普通串口接收中,数据是线性接收的,通常是通过中断或轮询的方式处理数据。

优势:

  1. 实现简单:直接读取数据,不需要复杂的指针管理。
  2. 适用于小数据量:对于数据量较小的场景,可以直接处理。

劣势:

  1. 低效率:如果处理速度不够快,容易造成数据丢失。
  2. 不适合大数据量:在大数据量场景下,容易出现缓存区满的情况。

环形缓存区的优势

  1. 适合实时数据处理:特别适用于需要持续接收和处理数据的应用,如串口通信。
  2. 高效利用缓存空间:可以更好地管理数据,减少数据丢失的风险。

总结

底层代码(DMA+空闲中断+RingBuffer)

HAL库API介绍

HAL_UART_RxCpltCallback()

功能

HAL_UART_RxCaltCallback函数是一个回调函数,用于处理UART接收完成事件。通常在接收完成时被调用。

描述

HAL_UART_RxCpltCallback函数的定义如下:

c
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart);

参数

  • huart:指向UART句柄的指针,类型为UART_HandleTypeDef *。

返回值

无返回值

使用场景

HAL_UART_RxCpltCallback函数通常用于以下场景:

  • 数据接收完成处理:在接收完成时对接收到的数据进行处理。
  • 事件触发操作:在接收完成时执行特定操作,例如通知任务、触发其他外设操作等。

实现原理

HAL_UART_RxCpltCallback函数的实现原理涉及在接收完成时,通过回调机制调用该函数。具体实现通常如下:

  1. 接收完成通知:当UART接收完成时,HAL库会调用该回调函数。
  2. 数据处理:在回调函数中对接收到的数据进行处理,可以从UART句柄中获取接收到的数据。
  3. 应用操作:根据应用需求执行特定的操作,例如将数据存储到缓冲区、触发其他任务或外设操作等。

HAL_UART_Receive_IT()

功能

HAL_UART_Receive_IT函数用于在中断模式下接收数据。它启动UART接收操作,并在接收到数据后触发中断。

描述

HAL_UART_Receive_IT函数的定义如下:

c
HAL_StatusTypeDef HAL_UART_Receive_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);

参数

  • huart:指向UART句柄的指针,类型为UART_HandleTypeDef *。
  • pData:指向接收数据缓冲区的指针,类型为uint8_t *。
  • Size:要接收的数据量,类型为uint16_t。

返回值

  • HAL_OK:函数调用成功。
  • HAL_ERROR:函数调用失败。
  • HAL_BUSY:函数调用时UART处于忙状态。
  • HAL_TIMEOUT:函数调用超时。

实现原理

HAL_UART_Receive_IT函数的实现原理涉及配置UART外设的中断和DMA控制器,并启动接收操作。具体实现通常如下:

  1. 配置中断:配置UART外设的接收中断,使能中断请求。
  2. 启动接收:设置接收缓冲区和数据长度,启动UART接收操作。
  3. 中断处理:在接收到数据后,触发中断处理函数,对接收到的数据进行处理。

HAL_UARTEx_ReceiveToIdle_DMA()

功能

HAL_UARTEx_ReceiveToIdle_DMA函数用于在DMA模式下接收数据,直到接收到空闲字符。它启动UART接收操作,并在接收到空闲字符后触发中断。

描述

HAL_UARTEx_ReceiveToIdle_DMA函数定义如下:

c
HAL_StatusTypeDef HAL_UARTEx_ReceiveToIdle_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);

参数

  • huart:指向UART句柄的指针,类型为UART_HandleTypeDef *。
  • pData:指向接收数据缓冲区的指针,类型为uint8_t *。
  • Size:要接收的数据量,类型为uint16_t。

返回值

  • HAL_OK:函数调用成功。
  • HAL_ERROR:函数调用失败。
  • HAL_BUSY:函数调用时UART处于忙状态。
  • HAL_TIMEOUT:函数调用超时。

实现原理

HAL_UARTEx_ReceiveToIdle_DMA函数的实现原理涉及配置UART外设的中断和DMA控制器,并启动接收操作。具体实现通常如下:

  1. 配置DMA:配置UART外设的接收中断,使能DMA请求。
  2. 启动接收:设置接收缓冲区和数据长度,启动UART接收操作。
  3. 中断处理:在接收到空闲字符后,触发中断处理函数,对接收到的数据进行处理。

__HAL_DMA_DISABLE_IT()

功能

__HAL_DMA_DISABLE_IT宏用于禁用指定的DMA中断。

描述

__HAL_DMA_DISABLE_IT宏的定义如下:

c
#define __HAL_DMA_DISABLE_IT(__HANDLE__,__INTERRUPT__)	((__HANDLE__) -> Instance -> CCR &= ~(__INTERRUPT__))

参数

  • HANDLE :指向DMA句柄的指针。
  • INTERRUPT:要禁用的中断,类型为uint32_t。

返回值

无返回值。

使用场景

__HAL_DMA_DISABLE_IT通常用于以下场景:

  • 中断控制:禁用特定的DMA中断。
  • DMA管理:在特定情况下禁用DMA中断以避免干扰。

实现原理

__HAL_DMA_DISABLE_IT的实现原理涉及直接操作DMA控制寄存器,清除指定中断位。具体实现如下:

  1. 访问寄存器:访问DMA控制寄存器。
  2. 清除中断位:使用位操作清除指定中断位。

HAL_UARTEx_RxEventCallback()

功能

HAL_UARTEx_RxEventCallback函数是一个回调函数,用于处理UART接收事件。它通常在接收到数据或发生特定接收事件时被调用。

描述

HAL_UARTEx_RxEventCallback函数的定义如下:

c
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size);

参数

  • huart:指向UART句柄的指针,类型为UART_HandleTypeDef *。
  • Size:要接收的数据量,类型为uint16_t。

返回值

无返回值。

使用场景

HAL_UARTEx_RxEventCallback函数通常用于以下场景:

  • 数据接收处理:处理接收到的数据,根据应用需求进行处理。
  • 事件触发操作:在特定接收事件发生时执行特定操作,例如记录日志、发送响应等。

实现原理

HAL_UARTEx_RxEventCallback函数的实现原理涉及在接收到数据或发生特定接收事件时,通过回调机制调用该函数。具体实现如下:

  1. 接收数据:当UART接收到数据或发生特定接收事件时,HAL库会调用该回调函数。
  2. 数据处理:在回调函数中对接收到的数据进行处理,可以根据Size参数获取接收到的数据大小。
  3. 应用操作:根据应用需求执行特定操作,例如将数据存储到缓冲区,触发其他外设操作等。

九、ADC模块

CubeMX配置

ADC Mode(ADC模式)

  • Independent Mode:独立模式,每个ADC独立工作。
  • Dual Mode:双重模式,两个ADC协同工作,提升采样速度。
  • Triple Mode:三重模式,三个ADC协同工作,进一步提升采样速度。

Resolution(分辨率)

  • 选择ADC的分辨率,如6位、8位、10位或12位。分辨率越高,精度越高,但转换时间越长。

Data Alignment(数据对齐)

  • 选择转换后数据的对齐方式:右对齐(Right Alignment)或左对齐(Left Alignment)。

Scan Conversion Mode(扫描转换模式)

  • Enable:使能扫描模式,可以对多个通道进行连续转换。
  • Disable:禁用扫描模式,仅转换一个通道。

Continuous Conversion Mode(连续转换模式)

  • Enable:使能连续转换模式,ADC会连续不断地进行转换。
  • Disable:禁用连续转换模式,每次触发只进行一次转换。

Discontinuous Conversion Mode(间歇转换模式)

  • Enable:使能间歇转换模式,允许每N个转换后暂停。
  • Disable:禁用间歇转换模式。

External Trigger Conversion Mode(外部触发转换模式)

  • Disable:禁用外部触发,转换由软件启动。
  • Rising Edge/Falling Edge/ Both Edges:选择外部触发信号的边沿。

Trigger Source(触发源)

  • 选择外部触发源,如定时器、外部引脚等。

Channel Configuration(通道配置)

  • 对每个启用的ADC通道进行配置,包括通道序号、采样时间等。

Sampling Time(采样时间)

  • 选择每个通道的采样时间。采样时间越长,输入信号的采样精度越高,但转换时间也越长。

DMA(Direct Memory Access, 直接存储器访问)

  • Enable:使能DMA, 自动将转换结果传输到内存,减轻CPU负担。
  • Disable:禁用DMA,转换结果由CPU读取。

Watchdog(看门狗)

  • Enable:使能看门狗功能,可以监控转换结果是否超出预设范围。
  • Disable:禁用看门狗功能。

Analog Watchdog Mode(模拟看门狗模式)

  • 选择看门狗模式:单通道、全部通道或扫描模式下的全部通道。

Overrun(超载)

  • Enable:使能超载保护,防止新的数据覆盖未读取的数据。
  • Disable:禁用超载保护。

底层代码

c
HAL_ADC_Start_DMA(&hadc1, (uint32_t *)&dma_buff[0][0], 30);
HAL_ADC_Start_DMA(&hadc2, (uint32_t *)&dma_buff[1][0], 30);


uint32_t dma_buff[2][30];
float adc_value[2];

void adc_proc(void)
{
	for(uint8_t i=0; i<30; i++)
	{
		adc_value[0] += (float)dma_buff[0][i];
		adc_value[1] += (float)dma_buff[1][i];
	}
	
	adc_value[0] = adc_value[0] / 30 * 3.3f / 4096;
	adc_value[1] = adc_value[1] / 30 * 3.3f / 4096;
}
  1. 参考电压:ADC将输入电压(通常为0到参考电压,假设为3.3V)转换为一个数字值。在大多数微控制器中,参考电压通常是3.3V,但也可以是其他电压值。

  2. 分辨率:ADC的分辨率决定了它可以输出多少种不同的数字值。例如,一个12位的ADC可以输出的数字范围是0到4095(即212 -1个可能的值)。

  3. 转换关系:

    • 假设输入电压为(V_in),ADC的参考电压为(V_ref),ADC的分辨率为N位(即有(2N)个可能的数字输出值),那么输入电压与数字输出值之间的关系为: $$ ADC_value = \left( \frac{V_{in}}{V_{ref}} \right) \times (2^N - 1) $$

    • 反过来,我们可以从ADC的数字输出值计算输入电压: $$ V_{in} = \left( \frac{ADC_value}{2^N - 1} \right) \times V_{ref} $$

    • 3.3f:表示ADC的参考电压是3.3V。

    • 4096:表示12位ADC的分辨率,即4096个不同的数字值。

      • 3.3f / 4096:这个计算将ADC的数字输出值转化为实际的输入电压值。

222333

HAL_GPIO_ReadPin()

功能

HAL_GPIO_ReadPin函数用于读取指定GPIO引脚的电平状态。它返回一个表示引脚当前状态的值,通常时高电平(GPIO_PIN_SET)或低电平(GPIO_PIN_RESET)。

描述

HAL_GPIO_ReadPin函数的定义如下:

c
GPIO_PinState HAL_GPIO_ReadPin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin);

参数

· GPIOx:指向GPIO端口的基地址,类型为GPIO_TypeDef*。比如GPIOA、GPIOB。

· GPIO_Pin:指定要读取的GPIO引脚,类型为uint16_t。可以是GPIO_PIN_0、GPIO_PIN_1等。

Released under the MIT License.