【STM32+CUBEMX+HAL库】RTC设备学习小结

什么是RTC设备?

RTC设备即real time clock的缩写,是一种掉电也能继续计时的计时器。虽然它只有简单的计时和触发中断的功能,但它掉电也能继续运行则让它的价值瞬间上升了无数倍。

CUBEmx配置

图一

RTC设备因为其独特的运行方式(即掉电依旧运行)导致它不能使用HSE或者HSI进行分频,否则资源消耗太大,小小的纽扣电池根本吃不消。所以我们可以选择单片机内部的LSI或者使用外部晶振LSE,推荐使用外部晶振LSE,因为单片机内部的LSI容易受到电压以及温度的影响导致精度不足。

在这里插入图片描述

在这里插入图片描述

以上为基本的一些设置。

在这里插入图片描述

在这里插入图片描述

以上分别是设置中断和设置系统日期。

在这里插入图片描述

在这里插入图片描述

因为接下来的例程会使用的串口和led灯所以要在cube中配置一下。

HAL库中有关RTC设备的API讲解

在编程之前我们需要先了解一下我们一会用到的api:

/*设置系统时间*/
HAL_StatusTypeDef HAL_RTC_SetTime(RTC_HandleTypeDef *hrtc, RTC_TimeTypeDef *sTime, uint32_t Format) 
/*读取系统时间*/
HAL_StatusTypeDef HAL_RTC_GetTime(RTC_HandleTypeDef *hrtc, RTC_TimeTypeDef *sTime, uint32_t Format)
/*设置系统日期*/
HAL_StatusTypeDef HAL_RTC_SetDate(RTC_HandleTypeDef *hrtc, RTC_DateTypeDef *sDate, uint32_t Format)
/*读取系统日期*/
HAL_StatusTypeDef HAL_RTC_GetDate(RTC_HandleTypeDef *hrtc, RTC_DateTypeDef *sDate, uint32_t Format)
/*启动报警功能*/
HAL_StatusTypeDef HAL_RTC_SetAlarm(RTC_HandleTypeDef *hrtc, RTC_AlarmTypeDef *sAlarm, uint32_t Format)
/*设置报警中断*/
HAL_StatusTypeDef HAL_RTC_SetAlarm_IT(RTC_HandleTypeDef *hrtc, RTC_AlarmTypeDef *sAlarm, uint32_t Format)
/*报警时间回调函数*/
__weak void HAL_RTC_AlarmAEventCallback(RTC_HandleTypeDef *hrtc)
/*写入后备储存器*/
void HAL_RTCEx_BKUPWrite(RTC_HandleTypeDef *hrtc, uint32_t BackupRegister, uint32_t Data)
/*读取后备储存器*/
uint32_t HAL_RTCEx_BKUPRead(RTC_HandleTypeDef *hrtc, uint32_t BackupRegister	

代码实战

接下来的代码会演示如何实时显示时间,设置rtc报警中断并启用中断,以及解决rtc掉电重启时间会重置的问题。
cubemx会自动帮我们生成初始化代码:

#include "rtc.h"
/* USER CODE BEGIN 0 */

/* USER CODE END 0 */
RTC_HandleTypeDef hrtc;
/* RTC 初始化函数 */
void MX_RTC_Init(void)
{
  RTC_TimeTypeDef sTime = {0};
  RTC_DateTypeDef DateToUpdate = {0};
  /** 初始化RTC */
  hrtc.Instance = RTC;
  hrtc.Init.AsynchPrediv = RTC_AUTO_1_SECOND;
  hrtc.Init.OutPut = RTC_OUTPUTSOURCE_NONE;
  if (HAL_RTC_Init(&hrtc) != HAL_OK)
  {
    Error_Handler();
  }
  /* USER CODE BEGIN Check_RTC_BKUP */
 
  /* USER CODE END Check_RTC_BKUP */

  /** 设置RTC系统时间以及日期  */
  sTime.Hours = 19;
  sTime.Minutes = 29;
  sTime.Seconds = 0;

  if (HAL_RTC_SetTime(&hrtc, &sTime, RTC_FORMAT_BIN) != HAL_OK)
  {
    Error_Handler();
  }
  DateToUpdate.WeekDay = RTC_WEEKDAY_SUNDAY;
  DateToUpdate.Month = RTC_MONTH_APRIL;
  DateToUpdate.Date = 4;
  DateToUpdate.Year = 20;

  if (HAL_RTC_SetDate(&hrtc, &DateToUpdate, RTC_FORMAT_BIN) != HAL_OK)
  {
    Error_Handler();
  }
}

void HAL_RTC_MspInit(RTC_HandleTypeDef* rtcHandle)
{
  if(rtcHandle->Instance==RTC)
  {
  /* USER CODE BEGIN RTC_MspInit 0 */

  /* USER CODE END RTC_MspInit 0 */
    HAL_PWR_EnableBkUpAccess();
    /* Enable BKP CLK enable for backup registers */
    __HAL_RCC_BKP_CLK_ENABLE();
    /* RTC clock enable */
    __HAL_RCC_RTC_ENABLE();

    /* RTC interrupt Init */
    HAL_NVIC_SetPriority(RTC_Alarm_IRQn, 0, 0);
    HAL_NVIC_EnableIRQ(RTC_Alarm_IRQn);
  /* USER CODE BEGIN RTC_MspInit 1 */

  /* USER CODE END RTC_MspInit 1 */
  }
}

void HAL_RTC_MspDeInit(RTC_HandleTypeDef* rtcHandle)
{
  if(rtcHandle->Instance==RTC)
  {
  /* USER CODE BEGIN RTC_MspDeInit 0 */

  /* USER CODE END RTC_MspDeInit 0 */
    /* Peripheral clock disable */
    __HAL_RCC_RTC_DISABLE();

    /* RTC interrupt Deinit */
    HAL_NVIC_DisableIRQ(RTC_IRQn);
    HAL_NVIC_DisableIRQ(RTC_Alarm_IRQn);
  /* USER CODE BEGIN RTC_MspDeInit 1 */

  /* USER CODE END RTC_MspDeInit 1 */
  }
} 
/* USER CODE BEGIN 1 */

/* USER CODE END 1 */

我已经把原本的英文注释翻译成了中文注释,简单的来说上面这些代码就是完成了RTC设备的初始化以及设置系统时间,并且初始化了中断。

接下来完成通过串口实时显示时间的功能:
首先为了方便接下来的使用我们需要把下面这个结构体变量转换为全局变量

RTC_TimeTypeDef sTime = {0};
#include "rtc.h"
/* USER CODE BEGIN 0 */
RTC_TimeTypeDef sTime = {0};
/* USER CODE END 0 */
RTC_HandleTypeDef hrtc;

然后点开我们的main函数,在while循环中读取当前时间,并且每秒通过串口传输到电脑上

#include "main.h"
#include "rtc.h"
#include "usart.h"
#include "gpio.h"

/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include "stdio.h"
/* USER CODE END Includes */
/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
/* USER CODE BEGIN PFP */

/* USER CODE END PFP */

/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
int fputc(int ch,FILE *f){
     uint8_t temp[1]={ch};
     HAL_UART_Transmit(&huart1,temp,1,2);
     return ch;
}
/* USER CODE END 0 */

/**
  * @brief  The application entry point.
  * @retval int
  */
int main(void)
{
  /* USER CODE BEGIN 1 */
extern RTC_TimeTypeDef sTime;
  /* USER CODE END 1 */
  
  /* MCU Configuration--------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();
  /* USER CODE BEGIN Init */

  /* USER CODE END Init */

  /* Configure the system clock */
  SystemClock_Config();
  /* USER CODE BEGIN SysInit */

  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_RTC_Init();
  MX_USART1_UART_Init();
  /* USER CODE BEGIN 2 */

  /* USER CODE END 2 */
 
  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    HAL_RTC_GetTime(&hrtc, &sTime, RTC_FORMAT_BIN);
    printf("%02d:",sTime.Hours);
    printf("%02d:",sTime.Minutes);
    printf("%02d\n",sTime.Seconds);
    HAL_Delay(1000);
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */
}

为了方便我们打印时间,我们改写了printf函数。

实现RTC报警中断功能:
首先在全局变量中定义结构体变量。

/* USER CODE BEGIN 0 */
RTC_TimeTypeDef sTime = {0};
RTC_AlarmTypeDef sAlarm ;
/* USER CODE END 0 */

让我们来看一下RTC_AlarmTypeDef这个结构体里面都有些什么?

在这里插入图片描述

在这里我们又看到了一个结构体名称,有没有觉得这个名称在哪见过?
没错!在最开始初始化RTC系统时间时我们有见到过它

在这里插入图片描述

继续查看这个结构体的内容发现:
在这里插入图片描述

原来设置报警的时间和设置系统的时间所用的数据结构是一样的。
搞清楚了报警结构体后设置报警中断就很容易了:

RTC_AlarmTypeDef sAlarm ;
void sAlarm_Config(int hours,int minutes,int seconds){
    /*填写报警结构体变量*/
  sAlarm.Alarm=RTC_ALARM_A; 
  sAlarm.AlarmTime.Hours=hours; 
  sAlarm.AlarmTime.Minutes=minutes;
  sAlarm.AlarmTime.Seconds=seconds;
  HAL_RTC_SetAlarm(&hrtc,&sAlarm, RTC_FORMAT_BIN);     //开启中断功能
  HAL_RTC_SetAlarm_IT(&hrtc,&sAlarm, RTC_FORMAT_BIN);  //设置中断
}

然后写一下回调函数:

void HAL_RTC_AlarmAEventCallback(RTC_HandleTypeDef *hrtc){
   HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_RESET);
}

接下来去main函数中调用一下:

#include "main.h"
#include "rtc.h"
#include "usart.h"
#include "gpio.h"

/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include "stdio.h"
/* USER CODE END Includes */
/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
/* USER CODE BEGIN PFP */

/* USER CODE END PFP */

/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
int fputc(int ch,FILE *f){
     uint8_t temp[1]={ch};
     HAL_UART_Transmit(&huart1,temp,1,2);
     return ch;
}
void sAlarm_Config(int hours,int minutes,int seconds) //声明中断函数
/* USER CODE END 0 */

/**
  * @brief  The application entry point.
  * @retval int
  */
int main(void)
{
  /* USER CODE BEGIN 1 */
extern RTC_TimeTypeDef sTime;
  /* USER CODE END 1 */
  
  /* MCU Configuration--------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();
  /* USER CODE BEGIN Init */

  /* USER CODE END Init */

  /* Configure the system clock */
  SystemClock_Config();
  /* USER CODE BEGIN SysInit */

  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_RTC_Init();
  MX_USART1_UART_Init();
  /* USER CODE BEGIN 2 */
void sAlarm_Config(19,30,0); //在1分钟后发送中断
  /* USER CODE END 2 */
 
  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    HAL_RTC_GetTime(&hrtc, &sTime, RTC_FORMAT_BIN);
    printf("%02d:",sTime.Hours);
    printf("%02d:",sTime.Minutes);
    printf("%02d\n",sTime.Seconds);
    HAL_Delay(1000);
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }
  /* USER CODE 

当然,如果我们要完成的是在5小时候后发送中断我们可以把中断函数改写成这样:

void sAlarm_Config2(int hours,int minutes,int seconds){
  HAL_RTC_GetTime(&hrtc, &sTime, RTC_FORMAT_BIN);   //获取设置中断时的时间
  sAlarm.Alarm=RTC_ALARM_A;
  sAlarm.AlarmTime.Hours=hours+sTime.Hours;
  sAlarm.AlarmTime.Minutes=minutes+sTime.Minutes;
  sAlarm.AlarmTime.Seconds=seconds+sTime.Seconds;
  HAL_RTC_SetAlarm(&hrtc,&sAlarm, RTC_FORMAT_BIN);
  HAL_RTC_SetAlarm_IT(&hrtc,&sAlarm, RTC_FORMAT_BIN);
}

在这里要注意一下,因为我们设置的是多少时间后进行中断,那么我们就要把当前时间加上多少我们要求的时间,所以要先获取一下当前时间是多少,如果不获取一下,结构体里面的变量就都是0。具体请看以下代码:

在这里插入图片描述

在这里插入图片描述

我们从HAL_RTC_GetTime()这个函数内容可以看出sTime结构体是通过RTC_ReadTimeCounter()这个函数读取了当前RTC设备下的RTC_CNT寄存器中的值。如果不读取的话,这个结构体里面的值就为0。
另外,我在看操作手册时看到了以下这段话:在这里插入图片描述

我最开始以为就是操作手册中的这个原因,但是我分析一下后发现,应该不是这个原因,因为在这里应该是结构体中的变量没有被赋值导致的数据错误,而不是硬件上的问题,虽然结果都是0。如果有人发现我的分析是错误的话,非常欢迎在下面评论中指出我的错误。

好了言归正传,接下来我们还要解决掉电后,日期被重置的问题。
首先在创建如下函数:

void user_CheckRtcBkup(){
   HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR1, 1); 
}

然后在RTC初始化函数中加入这句:

/* USER CODE BEGIN Check_RTC_BKUP */
   if(HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR1)==1){
       return;
   }
  /* USER CODE END Check_RTC_BKUP */

然后在main函数中调用一下:

int main(void)
{
  /* USER CODE BEGIN 1 */
extern RTC_TimeTypeDef sTime;
  /* USER CODE END 1 */
  
  /* MCU Configuration--------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* USER CODE BEGIN Init */

  /* USER CODE END Init */

  /* Configure the system clock */
  SystemClock_Config();

  /* USER CODE BEGIN SysInit */

  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_RTC_Init();
  MX_USART1_UART_Init();
  /* USER CODE BEGIN 2 */
  user_CheckRtcBkup();
  sAlarm_Config2(0,0,10);
  user_sendmessage();
  /* USER CODE END 2 */
  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    HAL_RTC_GetTime(&hrtc, &sTime, RTC_FORMAT_BIN);
    printf("%02d:",sTime.Hours);
    printf("%02d:",sTime.Minutes);
    printf("%02d\n",sTime.Seconds);
    HAL_Delay(1000);
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */
}

这样我们就在初始化好RTC设备后,在后备寄存器中写入一个值1,这个后备寄存器在断电重启后值依旧不会被改变,所以在断电重启后再一次初始化时,RTC初始化函数中就不会把最开始设置的系统时间赋给sTime这个结构体。

*额外功能

当然我们在真实的开发中回调函数不大可能就单单只亮个灯,就比如我要在12小时后开启电磁阀,首先我要输出一个高电平(假设),然后在几秒钟后我们还要让电磁阀关闭,那我们相对的是不是还要输出一个低电平。以下代码完成了这部分的功能:

void HAL_RTC_AlarmAEventCallback(RTC_HandleTypeDef *nhrtc){
  if(HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_0)==GPIO_PIN_SET){  
   HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_0);  //翻转led灯的电平
   sAlarm.AlarmTime.Hours=sTime.Hours;
   sAlarm.AlarmTime.Minutes=sTime.Minutes;
   sAlarm.AlarmTime.Seconds=sTime.Seconds+5;  //设置报警时间为5秒后
   HAL_RTC_SetAlarm_IT(&hrtc,&sAlarm,RTC_FORMAT_BIN);
  return;  //这个return很关键,在设置完中断后要马上退出回调函数,否则led灯会在下个if中再次翻转
  }
  if(HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_0)==GPIO_PIN_RESET){
  HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_0);
  return;
  }
}

当然我这里还是用led灯来演示了,毕竟C语言入门hello world,单片机入门点亮led灯。具体思路就是首先检测电平,如果为高(我这块开发板低电平点亮led灯),就代表led灯是暗着的,那我们就翻转电平,并且设置中断时间为5秒后。设置完后直接退出当前回调函数,5秒后中断触发,这时电平由于是低电平那么我们就再次翻转一下,但是不再设置下一次中断。

HAL库与stm32CUBEMX学习记录——UART设备(1)

由于自己忘性比较大,所以打算花点时间把近期学的一些知识点给整理顺便巩固一下,话不多说直接开始。

一、什么时UART?

uart就是通用异步收发传输设备的简称,从名字里我们可以捕捉到几个关键词:异步、收发。那么也就是说UART没有时钟线或者时钟校准这一说因为它是异步的,而且它不仅可以收信息也可以发信息。因为这一简单实用的特点,在一些数据传输时UART被大量使用。

二、用cubemx配置UART

首先还是选择芯片型号啥的,这里我就不截图了。

然后点开connectivity,所有和通讯相关的大部分都在这儿,

点开后我们会发现不仅有uart还有usart,这两个其实没啥区别,区别就在于uart是异步传输,而usart不仅支持异步传输还支持同步传输。结合我的开发板所以我选择了pa10和pa9两个引脚作为串口设备,也理所应当地选择了usart1。

点开Mode界面我们会发现一堆模式:

从上到下分别是:异步模式

同步模式

单线模式

多处理器通讯

红外线传感器

LIN(一种运用于汽车电子设备通讯协议)

智能卡

有计时功能的智能卡

我们在这里使用第一个异步模式,软件自动为我们选择了两个引脚pa10和pa9,其中pa10作为RX(收信息),pa9作为TX(发信息)。

设置完后再去设置一下系统时钟,因为待会我们会使用串口进行中断所以我们还要使能一下NVIC(中断),最后生成一下代码。

三、HAL库函数讲解

在HAL库中关于UART设备的函数有很多,我会挑选常用且一会使用到的函数进行讲解。

我们打开stm32f1xx_hal_uart.c文件,会大体看到st公司把这么多关于uart的函数分为了4个部分:

1、Initialization and Configuration functions(初始化配置功能)

2、IO operation functions(IO操作功能)

3、Peripheral Control functions(外围操作功能)

4、Peripheral State and Errors functions(外围状态和操作功能)

我们接下来会把重点放在1和2中,3和4都是关于外设的如单线模式、LIN和多处理器之间的通讯。

1是用作初始化功能的函数,在使用标准库时这些是我们自己要进行配置的,但是有了cubemx的帮助这部分我们几乎不用管。

2才是我们真正用来串口通讯的函数,在进行通讯前我们要选择以堵塞方式还是非堵塞方式进行通讯。关于阻塞和非阻塞我也并不能说是很懂,个人感觉阻塞就是传递信息这个过程没有结束那么接下来的任务都不会进行直到这个过程结束,非阻塞就是不管你信息有没有传递完毕都会返回一个值。

阻塞和非阻塞之间的区别给我最大的感受就是,非阻塞有中断功能而阻塞没有。。。。。。而且st官方也对阻塞和非阻塞进行了解释,但是这个解释感觉并没有把阻塞与非阻塞之间的区别给清晰的写出来,也有可能是我语文理解不大好。但是st官方把大篇幅都花在了非阻塞上,而且关于非阻塞的api比阻塞的api明显多了很多(阻塞的只有2个。。。),再根据我的实践得出能用非阻塞就用非阻塞,阻塞在处理信息时有很大的误差,而非阻塞由于有中断功能所以处理信息会高效的很多而且错误率也低,所以我在使用时遵循以下这个规则:由单片机发出的信息使用阻塞式,向单片机发送的信息使用非阻塞式。

在解决完阻塞与非阻塞的问题后让我们来看一下常用的api有哪些:

(+) HAL_UART_Transmit();//阻塞模式发送
(+) HAL_UART_Receive();//阻塞模式接收

(+) HAL_UART_Transmit_IT();//非阻塞中断模式下发送
(+) HAL_UART_Receive_IT();//非阻塞中断模式下接收
(+) HAL_UART_IRQHandler();//中断回调函数句柄

(+) HAL_UART_RxCpltCallback();//中断回调函数

以上这些api可以帮你完成大部分的数据收发功能,接下来我会编写一个实例,实例可以做到接收数据并进行判断,判断成功会发送数据。

uint8_t message_receive[50];

void User_transmite(){
  
  uint8_t message[]="hello,this is SAT8";
HAL_UART_Transmit(&huart1,message,sizeof(message),50);
}

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart){
   if(huart == &huart1){
     	uint16_t num1=0,num2=0;
  
  uint8_t message1[]="hello,you are my wife,right?";
  uint8_t message2[]="who are you...";
  for(int i=0;i<=sizeof(message1);i++){
      if(message_receive[i]== message1[i]){
     				num1++;
  }
      else {
        break;
      }
  }
  for(int i=0;i<=14;i++){
     if(message_receive[i]==message2[i]){
      num2++;
  }
    else{
      break;
    }
  }
  if(num1==sizeof(message1)-1){
       uint8_t shy_word[]="yes...i am your wife";
     HAL_UART_Transmit(&huart1,shy_word,sizeof(shy_word),50);
     }
  if(num2==sizeof(message2)-1){
      uint8_t angry_word[]="you forget your wife?fxxk!";
    HAL_UART_Transmit(&huart1,angry_word,sizeof(angry_word),50);
  }
     
  }
}

void User_receive(){
  HAL_UART_Receive_IT(&huart1,message_receive,30);
  HAL_Delay(100);
}
int main(void)
{
  /* USER CODE BEGIN 1 */

  /* USER CODE END 1 */
  

  /* MCU Configuration--------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* USER CODE BEGIN Init */

  /* USER CODE END Init */

  /* Configure the system clock */
  SystemClock_Config();

  /* USER CODE BEGIN SysInit */

  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_USART1_UART_Init();
  
  /* USER CODE BEGIN 2 */
  HAL_UART_IRQHandler(&huart1);
  User_transmite();
  /* USER CODE END 2 */
 
 

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    User_receive();
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */
}

调试成功,有一点要说明一下,

void User_receive(){
  HAL_UART_Receive_IT(&huart1,message_receive,30);
  HAL_Delay(100);
}

在这个接收函数中我添加了一个延时,时间为100ms。具体为什么是因为如果不加,在两次发送讯息之间时间如果过短会导致数据接收不到,具体原因还不清楚,但是一加上去后在两次快速发送讯息时导致的数据接收问题就大大改善了,如果有人能够回答一下的话就太好了。

 

openCV:使用均值模糊和高斯模糊来对图像进行处理

模糊又称平滑,是一种去噪手段,接下来我们会介绍均值模糊和高斯模糊。

均值模糊

我们会先创建一个卷积核,这个卷积核可以是3*3或者任意的x*x,如下图:

在创建完一个3*3卷积核后我们会将黄色格子和红色格子的像素值给加起来然后除以9(也就是卷积核里有多少个像素点就除以多少),这样我们就得出了平均值,然后将平均值赋给红色格子,这样我们就对一个像素完成了模糊处理。

所谓模糊就是平滑,只要让所有的像素值都趋于一个平均值就能达到平滑的效果了。接着我们就对下一个像素点进行同样的处理直到整张图片都完成处理。

openCV给出了均值模糊的API:

void blur( InputArray src, OutputArray dst,
 Size ksize, Point anchor = Point(-1,-1),
 int borderType = BORDER_DEFAULT );

src:输入图像

dst:输出图像

ksize:卷积核大小

Point anchor:锚点,最后得出的平均值所赋给的像素点坐标也就是上文所提到的红色格子,建议不要轻易改变这里的数值,默认数值为(-1,-1)

borderType: 用于推断图像外部边缘像素,有默认值BORDER_DEFAULT,这个参数是什么意思呢?就是我们在使用卷积核时是从第2行第2列开始的,那么问题来了第一行和第一列怎么办?难道我们就不管了?当然不可能,我们对边缘处理有以下几种办法:

  • ① 对称处理,就是把已有的点拷贝到另一面的对应位置,模拟出完整的矩阵。
  • ② 赋0,想象图像是无限长的图像的一部分,除了我们给定值的部分,其他部分的像素值都是0
  • ③ 赋边界值,想象图像是无限制长,但是默认赋值的不是0而是对应边界点的值

示例

#include<opencv2/opencv.hpp>
#include<stdc++.h>
using namespace cv;
using namespace std;

int main() {
  Mat a, b;
  a = imread("D:\\壁纸\\带带大师兄.jpg");
  if (!a.data) {
    cout << " 没有找到图片";
    waitKey(0);
    return -1;
  }
  imshow("清晰的孙笑川", a);
  
  blur(a, b, Size(9, 9));
  imshow("模糊的孙笑川", b);
  
  waitKey(0);
  return 0;
}

运行结果如下:

高斯模糊

均值模糊就是通过求平均值来进行模糊操作,这样的结果会使我们最后得到的图像不够平滑,而且更加容易受到噪声的干扰。

而高斯模糊则是通过线性滤波方式进行去噪,通过权重的方式来进行平均值的计算,什么意思呢?让我们看下图:

图中的数字代表每个像素点的像素值,这是让我们引入一个公式:

这是二维高斯函数以此来表明在正态分布上每个像素点的权重,

通俗点讲就是越靠近锚点的权重越大,越远离锚点的权重越小。通过加权平均数我们可以得到更平滑的模糊图像。

openCV给出了高斯模糊的api:

void GaussianBlur( InputArray src, OutputArray dst, Size ksize,
                                double sigmaX, double sigmaY = 0,
                                int borderType = BORDER_DEFAULT );

src:输入图像

dst:输出图像

ksize:卷积核大小,这里我们要注意一下在使用高斯模糊时卷积核大小只能是奇数*奇数

sigmax:在X方向的标准差,通常情况下sigmax与sigmay的值是一样的,而且openCV允许在这两个值中填写0,这样openCV会自动算出每个点的权重。

sigmay:在Y方向的标准差

borderTypr:用于推断图像外部边缘像素,有默认值BORDER_DEFAULT。

示例

#include<opencv2/opencv.hpp>
#include<stdc++.h>
using namespace cv;
using namespace std;

int main() {
  Mat a, b;
  int height,width;
  a = imread("D:\\壁纸\\带带大师兄.jpg");
  if (!a.data) {
    cout << " 没有找到图片";
    waitKey(0);
    return -1;
  }
  imshow("清晰的孙笑川", a);
  GaussianBlur(a, b, Size(9, 9), 0, 0);
  imshow("模糊的孙笑川", b);

  waitKey(0);
  return 0;
}

运行结果如下:

为了让各位对均值模糊和高斯模糊有个更直观的认识所以我展示一下均值模糊和高斯模糊的对比:

  影流之主

使用openCV来完成图像的叠加

在openCV中我们可以使用以下函数来进行图像的叠加:

addWeighted(InputArray src1, double alpha, InputArray src2, double beta, double gamma, OutputArray dst);

接下来让我们来好好分析一下这个函数:

src1:需要叠加的第一个图像

alpha:src1图像在叠加时所占的权重

src2:需要叠加的另一个图像

beta:src2图像在叠加式所占的权重

gamma:叠加完后加的常数,你可以增加gamma的值让图片更亮

dst:叠加完后输出的图像

示例

#include<opencv2/opencv.hpp>
#include<stdc++.h>
using namespace cv;
using namespace std;

int main() {

  Mat a,b,c;
  /*读取图片*/
  a = imread("D:\\壁纸\\带带大师兄.jpg");
  if (!a.data) {
    printf("没有找到图片");
    return -1;
  }
  b = imread("D:\\壁纸\\星空.jpg");
  if (!b.data) {
    printf("没有找到图片");
    return -1;
  }	
  /*当大小一致时直接进行叠加*/
  if (a.size() == b.size()) {
    addWeighted(a, 0.4, b, 0.6,0, c);
    imshow("jun", c);
  }
  /*当大小不一致时先改变大小至一致再进行叠加*/
  else 
  {
    Mat bnew;
    int row, col;
    row = a.rows;
    col = a.cols;
    resize(b, bnew, Size(col, row));			
    addWeighted(a, 0.2, bnew, 0.8, 0, c);
    imshow("jun", c);		
  }
  
  waitKey(0);
  return 0;
}

在使用addWeighted()函数时要注意两张图片的大小必须相等,所以我们在进行叠加图片前要判断两者大小是否相等。这里介绍下resize()函数:

resize( InputArray src, OutputArray dst,Size dsize, double fx = 0, double fy = 0, int interpolation = INTER_LINEAR )

我们来分析一下这个函数:

src:输入图像

dst:输出图像

dsize:改变后图像的大小,如果选择填写此参数那么我们就要填写精确的数值,如Size(30,30),这就代表改变后的图像大小为30*30。如果你不选择填写此参数你可以填写0,那么你就需要填写fx、fy参数。

fx、fy:如果你无法确定被改变大小的图像要改变的确切数值你可以填写此参数,此参数会根据你填写的数值进行放大或缩小,如:

resize(jun,bian,Size(),2,2,INTER_LINEAR);

这样我们就能把jun这个图像按x轴放到2倍,按y轴放大2倍,并得到新的图像bian。

interpolation:此参数是用来选择使用什么插值法来进行变换图片大小。不论是放大或是缩小图像都需要插值运算,缩小图像时,目标图像的像素会映射为源图像中的多个像素,放大图像时,目标图形上的像素可能无法在源图像中找到精确对应的像素,都需要进行插值运算。

当然了,你可以将此参数空着,这样就会选择默认的双线性插值法。

以下为你可以选择的插值法:

1)INTER_NEAREST – 最近邻插值法
2)INTER_LINEAR – 双线性插值法(默认)
3)INTER_AREA – 基于局部像素的重采样(resampling using pixel area relation)。对于图像抽取(image decimation)来说,这可能是一个更好的方法。但如果是放大图像时,它和最近邻法的效果类似。
4)INTER_CUBIC – 基于4×4像素邻域的3次插值法
5)INTER_LANCZOS4 – 基于8×8像素邻域的Lanczos插值

使用resize()函数我们就能让a图像和b图像大小一致了,接下来就是使用addWeighted()函数进行叠加操作了。

让我们来运行一下试试:

我们可以看到两幅图片叠加到一起了!是不是觉得很儒雅随和呢?

 

openCV:Mat对象详解和使用

Mat对象

Mat是OpenCV中用来存储图像信息的内存对象,可以理解为一个包含所有强度值的像素点矩阵,另外包含其他信息(宽,高,类型,纬度,大小,深度等)。

当你使用Mat对象时会自动为你分配内存空间,不用再像openCV2.x.x版本之前一样使用IplImage还要自己分配内存空间。

Mat对象使用

首先是最简单的使用:

Mat a;
a = imread("...");

定义一个Mat对象a后使用imread函数读取一张图片。

我们还能这样使用Mat:

Mat a,b;
a=imread("...");
b=Mat(a.size(),a.type());

首先定义一个Mat对象a并读取一张图片后定义一个Mat对象b,并且这个对象b的大小和格式与a是一样的。

Mat a,b;
a=imread("...");
b=Mat(a.size(),a.type());
b=Scalar(255,255,255);

因为我们刚才只是定义了一个Mat对象b的框架所以我们可以使用Scalar函数来让b这个对象生成一张图片,图片的颜色为白色(因为三原色的值都为255所以是白色)。

Mat a,b;
a=imread("...");
b=a.clon();

如果我们想要完全复制一张照片要怎么办呢?这时我们就可以使用clon()这个函数了,clon()是全复制你不需要担心因为改变了对象a的数值而会导致对象b的数值随着a改变而改变。

Mat a,b;
a = imread("..");
a.copyTo(b);

copyTo()函数和clon()函数也是一样的效果,你可以根据自己的喜好去使用。

Mat a,b;
a = imread("..");
cvtColor(a,b,COLOR_BGR2GRAY);

cvtColor()函数可以将a复制到b并且以COLOR_BGR2GRAY的形式展现给你,在这里COLOR_BGR2GRAY是参数名称灰度图的意思。你可以根据需要使用其它参数。

Mat a(3,3,CV_8UC3,Scalar(50,100,200);
cout<<a<<endl;
namedWindow("test",CV_WINDOW_AUTOSIZE);
imshow("test",a);

我们同样可以使用Mat对象创建一个x*x的矩阵,并根据参数CV_8UC3来为它每个像素点来设置数值为(50,100,200)。需要解释一下的是C3代表的是Channel 3,意味着我们这张图片每个像素点有3个通道分别对应的是三原色(RGB),而后面的数值(50,100,200)就是三原色的数值(当然你可以根据需要自己变)。

除了三通道还有一通道,也就是灰度图(还记得上文哪有提到过🐎?)

Mat a = Mat::eye(5, 5, CV_8UC1);

我们可以通过上面这种形式来对对象a进行初始化,这里的eye代表最后出现的矩阵是以对角线形式出现的

你可以修改参数为zeros,这样它就会把矩阵通通初始化为0:

 

Mat对象差不多就讲到这儿了,光看没有用建议打开电脑自己也来操作一遍这样印象才深刻嗷!

如何在openCV中使用掩膜来完成对图像的处理

什么是掩膜?

在本篇文章开始前我们需要先来了解一下什么是掩膜,掩膜就是用选定的图像、图形或物体,对处理的图像(全部或局部)进行遮挡,来控制图像处理的区域或处理过程。用于覆盖的特定图像或物体称为掩模或模板。

掩膜通常是个n*n的矩阵,我们可以通过掩膜来更好地处理图像。

代码解析

首先贴上源码,关于代码的解析我已经写好了注释:

#include<opencv2/opencv.hpp>
#include<stdc++.h>

using namespace std;
using namespace cv;
int main() {
  Mat jun = imread("D:\\壁纸\\jun.png");//读取图片
  namedWindow("junbian", CV_WINDOW_AUTOSIZE);//创建窗口
  imshow("junbian", jun);//在窗口上显示图片

  Mat bian;
/* 创建一个掩膜 */
  Mat kernel = (Mat_<char>(3,3)<<0, -1,  0,
                              -1, 5, -1, 
                               0,-1, 0);
/* 使用filter2D来对图像进行处理 */
  filter2D(jun, bian, -1, kernel);

  namedWindow("cool jun", CV_WINDOW_AUTOSIZE);
  imshow("cool jun", bian);

  waitKey(0);
  return 0;
}

让我们来看看结果:

我们会发现处理过后的图像相较于原图对比度增强了。

接下来让我们来换一个掩膜试试看:

#include<opencv2/opencv.hpp>
#include<stdc++.h>

using namespace std;
using namespace cv;
int main() {
  Mat jun = imread("D:\\壁纸\\jun.png");//读取图片
  namedWindow("junbian", CV_WINDOW_AUTOSIZE);//创建窗口
  imshow("junbian", jun);//在窗口上显示图片

  Mat bian;
/* 创建一个掩膜 */
  Mat kernel = (Mat_<char>(3,3)<<-1, 0, -1,
                                  0, 4, 0, 
                                  -1,0,-1);
/* 使用filter2D来对图像进行处理 */
  filter2D(jun, bian, -1, kernel);

  namedWindow("cool jun", CV_WINDOW_AUTOSIZE);
  imshow("cool jun", bian);

  waitKey(0);
  return 0;
}

运行后的结果:

我们可以看到图像的轮廓被描绘了出来,在这里我们使用的掩膜是拉普拉斯滤波器,它可以将画面轮廓清晰地描绘出来。

如何在vs2015中配置openCV编译环境

(一)

从官网上下载openCV的exe文件,点击窗口按钮会自动进行下载。目前最新的版本有4.1.1和3.4.7,在本教程中将以3.4.7的版本作为例子。

(二)

下载完后我们会看到以下安装包,打开它选择安装路径后进行安装。

安装完毕后应该是这样的:

(三)

接下来我们来配置环境变量,好让vs2015更好地运行进程。

首先打开以下路径,然后复制。

接着打开控制面板——>高级系统设置——>环境变量。

点开环境变量——>Path——>编辑,然后把刚才复制的路径拷贝进去。

最后别忘记点确定。

(四)

然后我们要把我们所需要的头文件给包含进vs2015。

打开vs2015,新建一个控制台程序。

点击创建——>下一步——>勾选空项目——>完成。

接着打开属性管理器——>点击Microsoft.Cpp.x64.user的属性页——>包含目录——>编辑

找到我们头文件的路径然后点击“选择文件夹”,将所有路径都包含进去。

和刚才一样,我们点开库目录,并将路径包含进去。

同样的,我们点开链接器——>输入——>附加依赖项——>编辑

输入我们的路径,每个版本的名字都不一样大家可以在\openCV\build\x64\vc15\lib这个路径中查看你的是什么名字。

(五)

最后一步,我们将管理器配置成x64。

 

至此我们的开发环境就都配置成功啦!!!我们可以新建一个项目来检测我们是否配置成功。

新建一个项目,我们这里给新项目取名为jun,然后我们来读取一张照片。

说明我们的编译环境已经配置成功了。

物联网通讯协议——MQTT协议

什么是MQTT协议?

MQTT协议是构建与tcp/ip协议的一种应用层协议,我们常见的http就是一种应用层协议。MQTT协议的优点在于可以使用极少的代码和有限的带宽去传输信息,因为其低开销,低带宽占用的及时通讯特点使其在物联网,小型设备直接的通讯等中十分常见。

MQTT协议的格式

每条MQTT的消息都包含一个固定的报头,有些消息会携带一个可变报文头和一个负荷。

固定头

固定头顾名思义就是每个mqtt消息头部都要包含的头部,它最少可为两字节。

字节 7 6 5 4 3 2 1 0
字节1 讯息类型 DUP标志 QoS等级 保留
字节2 剩余长度

上表为我们展示了固定头的格式。

字节一:

讯息类型占了第一个字节中的第7位到第4位,

助记符 枚举 描述
Reserved 0 已预留
CONNECT 1 客户端请求连接到服务器
CONNACK 2 连接确认
PUBLISH 3 发布讯息
PUBACK 4 发布确认
PUBREC 5 发布收到(保证交付部分1)
PUBREL 6 发布版本(保证交付的第2部分)
PUBCOMP 7 发布完成(保证交付的第3部分)
SUBSCRIBE 8 客户订阅请求
SUBACK 9 订阅确认
UNSUBSCRIBE 10 客户退订请求
UNSUBACK 11 退订确认
PINGREQ 12 PING请求
PINGRESP 13 PING回应
DISCONNECT 14 客户端正在断开连接
Reserved 15 已预留

DUP标志占字节一中的第3位,

当客户端或服务器尝试重新传递PUBLISH,PUBREL, SUBSCRIBE或UNSUBSCRIBE消息时,将设置此标志。这适用于QoS值大于零(0)且需要确认的消息。当DUP位置1时,变量头包括消息ID。

接收者应将此标志视为有关消息是否先前已被接收的提示。不应依靠它来检测重复项。

QoS等级占了第2到第1位,它表示了此PUBLISH消息传递的服务质量。

QoS 价值 第二位 第一位 描述
0 0 0 最多一次 Fire and Forget(发送完后就不管了,不用管目标有没有收到) <=1
1 0 1 最少一次 Acknowledged delivery(接收端一定要确认收到信息,没收到就继续发,发到目标收到为止) >=1
2 1 0 只能一次 Assured delivery(不惜一切代价把信息一次性地交付给目标) =1
3 1 1 保留

保留位于第0位,

如果用户在发送给服务端的信息中将该位置“1”,服务器就会保留发给给客户端的信息。

字节二:

此字节表示当前消息中剩余的字节数,包括变量头和有效载荷中的数据。

一个字节可最多表示127个字节的消息,这里可能就有人问了一个字节最大是1111 1111按道理来说应该是256才对啊?因为mqtt协议最多可以以4字节的形式来表示剩余消息中的字节数,所以每个字节的第8位数字表示是否需要再使用一个字节来表示剩下消息中的字节数,如果在第8位置“1”就表示再使用一个字节,置“0”就表示不调用。

可变头

下表显示了变量头的格式示例:

Description 7 6 5 4 3 2 1 0
Protocol Name
byte 1 Length MSB (0) 0 0 0 0 0 0 0 0
byte 2 Length LSB (6) 0 0 0 0 0 1 1 0
byte 3 ‘M’ 0 1 0 0 1 1 0 1
byte 4 ‘Q’ 0 1 0 1 0 0 0 1
byte 5 ‘I’ 0 1 0 0 1 0 0 1
byte 6 ‘s’ 0 1 1 1 0 0 1 1
byte 7 ‘d’ 0 1 1 0 0 1 0 0
byte 8 ‘p’ 0 1 1 1 0 0 0 0
Protocol Version Number
byte 9 Version (3) 0 0 0 0 0 0 1 1
Connect Flags
byte 10 User name flag (1)
Password flag (1)
Will RETAIN (0)
Will QoS (01)
Will flag (1)
Clean Session (1)
1 1 0 0 1 1 1 x
Keep Alive timer
byte 11 Keep Alive MSB (0) 0 0 0 0 0 0 0 0
byte 12 Keep Alive LSB (10) 0 0 0 0 1 0 1 0

协议名称(Protocol Name)

协议名称位于MQTT CONNECT消息的变量标头中 。此字段是UTF编码的字符串。

协议版本(Protocol Version Number)

该字段是一个8位无符号值,表示客户端使用的协议的修订级别。

连接标志(Connect Flags)

Clean session,Will,Will QoS和保留标志存在于CONNECT消息的变量标头中。

清理会话标志(Clean Session)

位置:连接标志字节的位1。

如果设置为(0),则服务器必须在断开连接后存储客户端的订阅。这包括继续存储已订阅主题的QoS 1和QoS 2消息,以便在客户端重新连接时可以传递它们。服务器还必须在连接断开时保持正在传送的运行中消息的状态。在客户端重新连接之前,必须保留此信息。

如果设置为(1),则服务器必须丢弃有关客户端的任何先前维护的信息,并将连接视为“干净”。当客户端断开连接时,服务器还必须放弃任何状态。

预设置标志(Will flag)

位置:连接标志字节的位2。

Will消息定义当服务器在与客户端通信期间遇到I / O错误,或者客户端无法在Keep Alive计时器计划内进行通信时,服务器将代表客户端发布消息。服务器从客户端接收到DISCONNECT消息后,不会触发发送Will消息的操作。

如果设置了Will标志,则Will QoS和Will Retain字段必须出现在Connect标志字节中,而Will Topic和Will Message字段必须出现在有效负载中。

QoS

位置:连接标志字节的第4位和第3位。

如果客户端非自愿断开连接,则连接的客户端在Will消息的Will QoS字段中指定QoS级别。Will消息在CONNECT消息的有效负载中定义。

如果设置了Will标志,则Will QoS字段为必填字段,否则其值将被忽略。

Will QoS的值为0(0x00),1(0x01)或2(0x02)。下表显示了Will QoS标志。

将保留标志(will retain)

位置:连接标志字节的第5位。

Will Retain标志表示客户端是否意外断开连接时,服务器是否应保留由服务器代表客户端发布的Will消息。

如果设置了Will标志,则Will Retain标志是必需的,否则将被忽略。

用户名和密码标志(username and password flag)

位置:连接标志字节的第6位和第7位。

连接客户端可以指定用户名和密码,并且设置标志位表示CONNECT消息的有效负载中包括用户名和可选的密码。

如果设置了“用户名”标志,则“用户名”字段是必填字段,否则将忽略其值。如果设置了密码标志,则密码字段是必填字段,否则其值将被忽略。在不提供用户名的情况下提供密码是无效的。

保持活动计时器(Keep Alive timer)

保持活动计时器(以秒为单位)定义了从客户端收到的消息之间的最大时间间隔。它使服务器能够检测到与客户端的网络连接已断开,而不必等待长时间的TCP / IP超时。客户有责任在每个“保持活动”时间段内发送消息。在此时间段内没有数据相关消息的情况下,客户端会发送PINGREQ消息,服务器会通过PINGRESP消息进行确认 。

保持活动计时器可以通过MSB和LSB的顺序进行设置,它们分别占有2个字节。

负荷

以下类型的MQTT命令消息具有有效负载:

连接

有效负载包含一个或多个UTF-8编码的字符串。他们为客户端指定了一个无歧义的标识符,一个Will主题和消息以及要使用的用户名和密码。除了第一个以外的所有都是可选的,它们的存在是根据变量头中的标志确定的。

订阅

有效负载包含客户端可以订阅的主题名称和QoS级别的列表。这些字符串是UTF编码的。

后退

有效负载包含已授权QoS级别的列表。这些是服务器管理员已允许客户端订阅特定主题名称的QoS级别。授予的QoS级别以与相应的SUBSCRIBE消息中的主题名称相同的顺序列出。

学累了?听首歌吧!

 

 

浅谈tcp/ip协议

什么是 TCP/IP?

TCP/IP 是供已连接因特网的计算机进行通信的通信协议。

TCP/IP 指传输控制协议/网际协议 (Transmission Control Protocol / Internet Protocol)。

TCP/IP 定义了电子设备(比如计算机)如何连入因特网,以及数据如何在它们之间传输的标准。

这张表就是tcp/ip模型和OSI模型的模型图。我们可以看到tcp/ip模型比osi模型更加精简一些。

tcp/ip协议中各层的作用

在我们刚刚贴出的表中可以看到,tcp/ip协议分为四层,分别是:应用层,传输层,网络层和数据链路层。

应用层

应用层是TCP/IP协议的第一层,是直接为应用进程提供服务的。对不同种类的应用程序它们会根据自己的需要来使用应用层的不同协议。应用层还能加密、解密、格式化数据。应用层可以建立或解除与其他节点的联系。常见的应用层协议有:HTTP,MQTT,SMTP等等。

大家平时在登陆网站的时候经常会以https作为开头,https是http协议的变种,区别在于http使用明文传输容易被破解,安全性不高,而https协议会对传输的内容进行加密,使得器安全性大大增加,所以https协议已经几乎取代了http协议(当然有些网站用的还是http……)。

运输层

作为TCP/IP协议的第二层,运输层在整个TCP/IP协议中起到了中流砥柱的作用。

它为两台主机上的应用程序提供端到端的通信。常见的传输层协议有:tcp,udp协议。

网络层

网络层是OSI参考模型中的第三层,介于传输层和数据链路层之间,它在数据链路层提供的两个相邻端点之间的数据帧的传送功能上,进一步管理网络中的数据通信,将数据设法从源端经过若干个中间节点传送到目的端,从而向运输层提供最基本的端到端的数据传送服务。

主要的协议有ip,ipx等协议。

数据链路层

数据链路层最基本的服务是将源计算机网络层来的数据可靠的传输到相邻节点的目标计算机的网络层。

主要协议有点对点协议(Point-to-Point Protocol);以太网(Ethernet);高级数据链路协议(High-Level Data Link Protocol);帧中继(Frame Relay);异步传输模式(Asynchronous Transfer Mode)。

数据通过一层层的流动最后会发送出去,接受的服务端也会按照tcp/ip协议来进行读取信息,从数据链路层开始直到应用层最后到我们手上的就是经过处理好后的信息了。

基于rt_thread的pwm设备应用实例

PWM概述

我们即将创建一个会变换颜色的呼吸灯的程序,在编写程序之前我觉得有必要先稍微讲解一下什么是pwm。

pwm的全称是Pulse Width Modulation,翻译过来就是脉冲宽度调制。

以上就是pwm的原理示意图。在T1这个时间段中属于高电平输出,在T2这个时间段中属于底电平输出。高电平在整个周期(T1+T2)中所占时间的比例就是占空比,通过对占空比的控制我们可以输出一段幅值相等的脉冲。

PWM控制亮度的原理

我们平时调节手机屏幕和灯的亮暗就是靠pwm来进行控制的,通过对占空比的控制我们可以让led灯进行快速的闪烁,这个闪烁因为过快导致我们的肉眼根本无法识别出来,在不停的闪烁中如果灭的持续时间长那么我们就会觉得屏幕变暗了,如果亮的时间长我们就会觉得屏幕变亮了。

编程思路

首先贴上本次要使用的函数:

rt_device_find()

rt_pwm_set()

rt_pwm_enable()

rt_pwm_disable()

函数 描述
rt_device_find() 根据 PWM 设备名称查找设备获取设备句柄
rt_pwm_set() 设置 PWM 周期和脉冲宽度
rt_pwm_enable() 使能 PWM 设备
rt_pwm_disable() 关闭 PWM 设备

通过不停对占空比进行有序的改变可以让灯的亮度由暗变亮再让灯由亮变暗,有两种方法可以完成这种操作,一个是使用for或者while循环,一个就是使用数组,在接下来的实例中我们是使用数组进行有序控制的。

代码

首先在这儿声明一下,这里使用的开发板以低电平输出亮,高电平输出暗,开发板中关于led的原理图如下:

#include <rtthread.h>
#include <rtdevice.h>
#include <board.h>

#define pwm        "pwm3"
int jun[300];//声明数组
int a=0;
/* 将占空比的值赋给数组 */
void pwm_sat8(){
   for(int i=100000;i>=0;i=i-1000){
  jun[a]=i;
   a++;
}
  
   for(int i=0;i<=100000;i=i+1000){
  jun[a]=i;
   a++;
}

  a=0;
  struct rt_device_pwm *pwm_dev;//设备句柄      
  pwm_dev = (struct rt_device_pwm *)rt_device_find(pwm);//查找设备
/* 完成红、绿、蓝亮度的依次变化 */
  while(1){
    
  for(a=0;a<=201;a++){
   rt_pwm_set(pwm_dev,2,100000,jun[a]);
   rt_pwm_enable(pwm_dev,2);
   rt_thread_mdelay(20);
  }
  
  for(a=0;a<=201;a++){
   rt_pwm_set(pwm_dev,3,100000,jun[a]);
   rt_pwm_enable(pwm_dev,3);
   rt_thread_mdelay(20);
  }
  
  for(a=0;a<=201;a++){
   rt_pwm_set(pwm_dev,4,100000,jun[a]);
   rt_pwm_enable(pwm_dev,4);
   rt_thread_mdelay(20);		
  }
  
    }
  
      }

MSH_CMD_EXPORT(pwm_sat8, pwm sample)

首先,我们定义了一个名字为jun,大小为300的数组。

我们在这里设置的周期是100000纳秒(也就是0.1ms),因为亮度的大小范围是0%——100%,所以我们将占空比从100000,每次以减1000的方式赋值给数组,再从0,以每次加1000的方式赋值给数组,这样子我们就得到了{100000,99000,98000…98000,99000,100000}的数组。

接着使用rt_device_find()获取设备句柄,在获取设备句柄之前我们可以看到有这么一行代码:

struct rt_device_pwm *pwm_dev;

这行代码我们先创建了一个结构体,并给这个结构体设置了一个指针。

因为rt_device_find()返回的是一个句柄所以我们要先强制类型转换,然后才能把返回的句柄赋给指针。

while(1){
  
for(a=0;a<=201;a++){
 rt_pwm_set(pwm_dev,2,100000,jun[a]);
 rt_pwm_enable(pwm_dev,2);
 rt_thread_mdelay(20);
}

for(a=0;a<=201;a++){
 rt_pwm_set(pwm_dev,3,100000,jun[a]);
 rt_pwm_enable(pwm_dev,3);
 rt_thread_mdelay(20);
}

for(a=0;a<=201;a++){
 rt_pwm_set(pwm_dev,4,100000,jun[a]);
 rt_pwm_enable(pwm_dev,4);
 rt_thread_mdelay(20);		
}

  }

以上代码就是实现交替变换颜色并且安照顺序调节亮度的代码。我们抽取第一段分析一下:

for(a=0;a<=201;a++){
   rt_pwm_set(pwm_dev,2,100000,jun[a]);
   rt_pwm_enable(pwm_dev,2);
   rt_thread_mdelay(20);
  }

通过rt_pwm_set()可以设置我们需要使用的pwm设备和需要使用的通道(不理解什么是通道?看看我刚发的led原理图),然后设置周期为100000,最后设置占空比,在上文中我们已经讲过我们是使用数组进行占空比设置的,所以设置为jun[a],通过for循环中的判断和更新变量来完成占空比从100000到0再从0到100000的变化。

在设置pwm设备后我们要使能pwm设备,最后加上延时函数rt_thread_mdelay(),这里我们设置延时为0.02毫秒。

*注意

因为本次使用的开发板是以低电平为输出led亮的,所以当你使用env工具配置好pwm设备后它会默认亮起来,为了解决这个问题我们可以在mian()函数中编写如下代码:

int main(){
struct rt_device_pwm *pwm_dev;      
  pwm_dev = (struct rt_device_pwm *)rt_device_find("pwm3");
  rt_pwm_set(pwm_dev,2,100000,100000);
  rt_pwm_enable(pwm_dev,2);
  rt_pwm_set(pwm_dev,3,100000,100000);
  rt_pwm_enable(pwm_dev,3);
  rt_pwm_set(pwm_dev,4,100000,100000);
  rt_pwm_enable(pwm_dev,4);
}

这样子我们就能在开始的时候把它给关闭了。

学习辛苦了,看点色图放松一下吧(/龇牙)