• [技术干货] 基于STM32设计的智能灌溉控制系统
    一、项目介绍随着现代农业的发展,人们对于水资源的合理利用越来越重视。而传统的灌溉方式往往存在着浪费水资源、劳动力投入大、效率低等问题。因此,设计一款智能灌溉控制系统,可以实现对灌溉水量的精准控制,增加水资源利用率,提高农业生产效率,具有广泛的应用前景。当前文章介绍一款高性能的智能灌溉控制系统的开发过程,可自动采集电压、电流、累计用水量,并根据用户需要实现自动灌溉、定时灌溉、周期灌溉和手动灌溉等多种模式,同时具备中控室控制、手机短信、现场遥控及现场手动等多种方式控制功能。该系统可以对现场温湿度限值进行设置和修改,并通过控制器或后台监控系统完成灌溉起始时间、停止时间、喷灌时间等参数设置。系统显示功能包括液晶屏以中文菜单方式显示现场采集数据以及后台监控系统配大屏幕显示器,图形、表格等多种形式动态显示整个灌溉区运行情况。同时,在电压、电流或者流量出现异常时,系统可以及时报警。该系统供电为220VAC,流量计量误差精度为2级,使用二维码或卡实现预付费功能,通讯使用4G与云平台连接。二、设计功能本系统采用STM32作为主控芯片,并通过AD模块采集电压、电流和流量等数据。同时,通过继电器控制灌溉设备的启停,使用PWM控制阀门的开合程度,从而实现精确控制灌溉水量。通信模块则采用4G模块与云平台连接,实现远程监控及控制功能。预付费模块则使用二维码或卡实现预付费功能,用户需在充值后才能使用该系统进行灌溉操作。系统软件设计包括采集程序、控制程序、前端程序和后台程序。其中,采集程序主要负责采集电压、电流、流量等数据,并将采集到的数据上传到云平台;控制程序主要负责控制灌溉设备的启停和阀门的开合程度,从而实现灌溉控制;前端程序主要负责实现中文菜单方式显示现场采集数据,并提供灌溉模式选择、参数设置等功能;后台程序主要负责实现大屏幕显示器、图形、表格等多种形式动态显示整个灌溉区运行情况。【1】硬件部分MCU:本系统采用STM32作为主控芯片,其具有高性能、低功耗等优点,可满足该系统的高要求。数据采集模块:本系统通过AD模块采集电压、电流和流量等数据,然后使用MCU进行处理,并将采集到的数据存储到Flash中。控制模块:本系统通过继电器控制灌溉设备的启停,同时使用PWM控制阀门的开合程度,从而实现精确控制灌溉水量。通信模块:本系统采用4G模块与云平台连接,实现远程监控及控制功能。预付费模块:本系统使用二维码或卡实现预付费功能,用户需在充值后才能使用该系统进行灌溉操作。【2】软件部分采集程序:本系统的采集程序主要负责采集电压、电流、流量等数据,并将采集到的数据上传到云平台。控制程序:本系统的控制程序主要负责控制灌溉设备的启停和阀门的开合程度,从而实现灌溉控制。前端程序:本系统的前端程序主要负责实现中文菜单方式显示现场采集数据,并提供灌溉模式选择、参数设置等功能。后台程序:本系统的后台程序主要负责实现大屏幕显示器、图形、表格等多种形式动态显示整个灌溉区运行情况。三、系统实现具体实现过程如下:(1)采集程序采集程序主要由AD模块和STM32芯片完成。AD模块采集电压、电流和流量等数据,经过滤波和放大处理后,传输到STM32芯片上。STM32芯片通过串口将采集到的数据上传到云平台,并存储在Flash中。(2)控制程序控制程序主要由继电器和PWM模块完成。继电器用于控制灌溉设备的启停,PWM模块则用于控制阀门的开合程度,从而实现精确控制灌溉水量。控制程序通过读取Flash中存储的参数,确定灌溉起始时间、停止时间、喷灌时间等操作流程,并根据实时采集到的数据进行动态调整,保证灌溉操作的准确性和稳定性。(3)前端程序前端程序主要是通过液晶屏以中文菜单方式显示现场采集数据,并提供灌溉模式选择、参数设置等功能。用户可以通过按键或触摸屏来进行操作,并实时查看灌溉操作的运行情况。此外,用户还可以通过手机短信、现场遥控或现场手动等方式对灌溉操作进行控制。(4)后台程序后台程序主要负责实现大屏幕显示器、图形、表格等多种形式动态显示整个灌溉区运行情况,同时还能够将采集到的数据进行分析和统计,为灌溉管理提供决策参考。四、核心代码【1】电机控制代码以下是STM32F103ZET6通过PWM控制直流电机转速的代码,并封装成子函数调用的示例:首先,需要在STM32CubeMX中配置TIM定时器和GPIO引脚,以及将PWM模式设置为嵌套边沿对齐模式,然后生成代码,并在main.c文件中添加以下代码: #include "main.h"  #include "stm32f1xx_hal.h"  ​  /* TIM handle structure */  TIM_HandleTypeDef htim;  ​  /* Function prototypes */  void PWM_Init(TIM_HandleTypeDef *htim, uint32_t channel);  void Set_Motor_Speed(TIM_HandleTypeDef *htim, uint32_t channel, uint16_t speed);  ​  int main(void)  {    /* Initialize the HAL Library */    HAL_Init();  ​    /* Initialize TIM2 PWM with a frequency of 10 kHz */    PWM_Init(&htim2, TIM_CHANNEL_1);  ​    /* Set the motor speed to 50% */    Set_Motor_Speed(&htim2, TIM_CHANNEL_1, 5000);  ​    while (1)   {      /* Infinite loop */   }  }  ​  /**    * @brief Initializes PWM output on specified TIM channel.    * @param htim: TIM handle structure.    * @param channel: TIM channel to be used for PWM output.    * @retval None    */  void PWM_Init(TIM_HandleTypeDef *htim, uint32_t channel)  {    TIM_OC_InitTypeDef sConfigOC = {0};  ​    /* Configure PWM output on specified TIM channel */    sConfigOC.OCMode       = TIM_OCMODE_PWM1;    sConfigOC.Pulse        = 0;    sConfigOC.OCPolarity   = TIM_OCPOLARITY_HIGH;    sConfigOC.OCFastMode   = TIM_OCFAST_DISABLE;    HAL_TIM_PWM_ConfigChannel(htim, &sConfigOC, channel);  ​    /* Start PWM output */    HAL_TIM_PWM_Start(htim, channel);  }  ​  /**    * @brief Sets the motor speed on specified TIM channel.    * @param htim: TIM handle structure.    * @param channel: TIM channel to be used for PWM output.    * @param speed: Motor speed in units of 1/10,000th of the maximum speed.    *               For example, a speed of 5000 would set the motor speed to 50%.    * @retval None    */  void Set_Motor_Speed(TIM_HandleTypeDef *htim, uint32_t channel, uint16_t speed)  {    uint16_t max_speed = htim->Init.Period;  ​    /* Ensure that speed is within range */    if (speed > max_speed)      speed = max_speed;  ​    /* Update PWM duty cycle */    __HAL_TIM_SET_COMPARE(htim, channel, speed);  }在以上代码中,定义了两个函数:PWM_Init和Set_Motor_Speed。PWM_Init用于初始化TIM定时器的PWM输出,并设置指定通道的PWM模式和默认占空比为0。Set_Motor_Speed用于设置电机的转速,其接收三个参数:TIM句柄结构体,指定的通道,以及电机的转速(单位为1/10,000最大速度)。该函数会将电机的转速转换为PWM占空比,并通过__HAL_TIM_SET_COMPARE函数更新PWM占空比。最后,可以按照以下步骤将代码封装成子函数调用:将以上代码复制到单独的.c文件中,并包含必要的头文件。在该文件中定义一个名为Motor_Control的函数,该函数接收三个参数:TIM句柄结构体,指定的通道,以及电机的转速。在Motor_Control函数中调用PWM_Init和Set_Motor_Speed函数,并传递相应的参数。在main函数中调用Motor_Control函数,传递相应的参数。以下是Motor_Control函数的示例代码: #include "motor_control.h"  ​  void Motor_Control(TIM_HandleTypeDef *htim, uint32_t channel, uint16_t speed)  {    /* Initialize PWM output on specified TIM channel */    PWM_Init(htim, channel);  ​    /* Set the motor speed */    Set_Motor_Speed(htim, channel, speed);  }在以上示例中,将PWM的初始化和设置电机转速的函数封装成了一个名为Motor_Control的函数。可以在需要控制电机转速的其他地方调用Motor_Control函数即可。注意,在调用Motor_Control函数之前,需要先定义并初始化TIM句柄结构体,并确保GPIO引脚已经正确配置为TIM模式。此外,如果需要控制多个电机,可以在Motor_Control函数中增加参数以区分不同的电机通道。以下是motor_control.h头文件的示例代码: cCopy Code#ifndef __MOTOR_CONTROL_H__  #define __MOTOR_CONTROL_H__  ​  #include "stm32f1xx_hal.h"  ​  /* Function prototypes */  void PWM_Init(TIM_HandleTypeDef *htim, uint32_t channel);  void Set_Motor_Speed(TIM_HandleTypeDef *htim, uint32_t channel, uint16_t speed);  void Motor_Control(TIM_HandleTypeDef *htim, uint32_t channel, uint16_t speed);  ​  #endif /* __MOTOR_CONTROL_H__ */在以上头文件中,声明了三个函数:PWM_Init,Set_Motor_Speed和Motor_Control,并包含必要的头文件。【2】电压、电流采集为了采集220V抽水电机的用电量和当前电压,当前使用STM32F103ZET6的ADC(模数转换器)来测量电压和电流,并通过乘法器计算电功率和电能。下面是实现方案和实现代码:选择合适的传感器: 为了测量电压,可以使用AC-AC变压器将220V交流电压降至低电平,再使用电阻分压器将电压信号调整在ADC的输入范围内。 为了测量电流,可以使用霍尔传感器或者电阻式传感器,将电流信号转换成电压信号,然后通过电阻分压器调整信号范围。配置ADC: 使用STM32CubeMX软件选择相应的引脚和配置ADC模块,设置采样频率、参考电压等参数。需要注意的是,ADC模块只能同时转换一路模拟信号,因此需要轮流采样电压和电流信号。计算电流、电压、功率和能量: 将电压和电流信号转换成数字值后,可以使用下面的公式计算电流、电压、功率和能量: Copy Code电流 = AD值 / 灵敏度  电压 = AD值 / 分压比  功率 = 电压 * 电流  能量 = 功率 * 时间其中,灵敏度是传感器的转换系数,分压比是电阻分压器的比值,时间可以通过定时器计算。输出数据: 将测量的电流、电压、功率和能量输出到串口或者LCD显示屏上。可以设置一个定时器,在一定时间间隔内输出一次数据。实现代码: #include "stm32f1xx_hal.h"  ​  ADC_HandleTypeDef hadc1;  TIM_HandleTypeDef htim2;  ​  void SystemClock_Config(void);  static void MX_GPIO_Init(void);  static void MX_ADC1_Init(void);  static void MX_TIM2_Init(void);  ​  uint16_t ad_val_ch1, ad_val_ch2;  float voltage, current, power, energy;  ​  int main(void)  {    HAL_Init();    SystemClock_Config();    MX_GPIO_Init();    MX_ADC1_Init();    MX_TIM2_Init();  ​    while (1)   {      // ADC采样电压信号      HAL_ADC_Start(&hadc1);      HAL_ADC_PollForConversion(&hadc1, 100);      ad_val_ch1 = HAL_ADC_GetValue(&hadc1);      voltage = ad_val_ch1 * 3.3 / 4096 * 10; // 假设分压比为10  ​      // ADC采样电流信号      HAL_TIM_Base_Start(&htim2);      HAL_ADC_Start(&hadc1);      HAL_ADC_PollForConversion(&hadc1, 100);      ad_val_ch2 = HAL_ADC_GetValue(&hadc1);      current = ad_val_ch2 * 3.3 / 4096 * 50; // 假设灵敏度为50mV/A  ​      // 计算功率和能量      power = voltage * current;      energy += power * 0.1; // 假设定时器时间间隔为100ms  ​      // 输出测量结果      printf("Voltage: %.2f V\r\n", voltage);      printf("Current: %.2f A\r\n", current);      printf("Power: %.2f W\r\n", power);      printf("Energy: %.2f J\r\n", energy);  ​      HAL_Delay(1000); // 假设数据输出间隔为1s   }  }  ​  void SystemClock_Config(void)  {    RCC_OscInitTypeDef RCC_OscInitStruct = {0};    RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};  ​    __HAL_RCC_PWR_CLK_ENABLE();    __HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_VOLTAGE_SCALE1);  ​    RCC_OscInitStruct.OscillatorType = RCCRCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSI;  RCC_OscInitStruct.HSIState = RCC_HSI_ON;  RCC_OscInitStruct.HSICalibrationValue = RCC_HSICALIBRATION_DEFAULT;  RCC_OscInitStruct.PLL.PLLState = RCC_PLL_NONE;  if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)  {  Error_Handler();  }  ​  RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_SYSCLK | RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2;  RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_HSI;  RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;  RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2;  RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;  if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_0) != HAL_OK)  {  Error_Handler();  }  }  ​  static void MX_ADC1_Init(void)  {  ADC_ChannelConfTypeDef sConfig = {0};  ​  __HAL_RCC_ADC1_CLK_ENABLE();  ​  hadc1.Instance = ADC1;  hadc1.Init.ScanConvMode = DISABLE;  hadc1.Init.ContinuousConvMode = ENABLE;  hadc1.Init.DiscontinuousConvMode = DISABLE;  hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START;  hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT;  hadc1.Init.NbrOfConversion = 1;  if (HAL_ADC_Init(&hadc1) != HAL_OK)  {  Error_Handler();  }  ​  sConfig.Channel = ADC_CHANNEL_0; // 假设测量电压的ADC通道为0  sConfig.Rank = ADC_REGULAR_RANK_1;  sConfig.SamplingTime = ADC_SAMPLETIME_13CYCLES_5;  if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK)  {  Error_Handler();  }  ​  sConfig.Channel = ADC_CHANNEL_1; // 假设测量电流的ADC通道为1  if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK)  {  Error_Handler();  }  }  ​  static void MX_TIM2_Init(void)  {  __HAL_RCC_TIM2_CLK_ENABLE();  ​  htim2.Instance = TIM2;  htim2.Init.Prescaler = 7200 - 1;  htim2.Init.CounterMode = TIM_COUNTERMODE_UP;  htim2.Init.Period = 10000 - 1; // 假设定时器时间间隔为100ms  if (HAL_TIM_Base_Init(&htim2) != HAL_OK)  {  Error_Handler();  }  }  ​  void Error_Handler(void)  {  while (1)  {  }  }  ​  #ifdef USE_FULL_ASSERT  ​  void assert_failed(char *file, uint32_t line)  {  }  ​  #endif
  • [技术干货] C语言代码封装MQTT协议报文,了解MQTT协议通信过程
    【1】MQTT协议介绍MQTT是一种轻量级的通信协议,适用于物联网(IoT)和低带宽网络环境。它基于一种“发布/订阅”模式,其中设备发送数据(也称为 “发布”)到经纪人(称为MQTT代理),这些数据被存储,并在需要时被转发给订阅者。这种方式简化了网络管理,允许多个设备在不同的网络条件下进行通信(包括延迟和带宽限制),并支持实时数据更新。它是开放的,可免费使用并易于实施。【2】MQTT协议报文字段介绍MQTT协议报文由两部分组成:固定报头和可变报头。固定报头的格式是统一的,其中包括了报文类型和剩余长度两个字段。可变报头的格式取决于报文类型。下面是MQTT协议中各个报文类型的可变报头字段说明。(1)CONNECT:MQTT连接请求报文CONNECT报文包括固定报头和可变报头两部分。其中,固定报头的第一个字节(即报文类型和标志位的组合)为0x10,表示这是一个CONNECT报文。可变报头包括了以下字段:协议名(Protocol Name):用于标识MQTT协议的名称,固定为字符串"MQTT";协议级别(Protocol Level):用于标识所使用的MQTT协议的版本号,一般情况下为4;连接标志(Connect Flags):用于设置各种连接选项,其中包括:用户名/密码(Username/Password):用于对连接进行身份验证;清理会话(Clean Session):表示客户端需要清除服务器上旧的Session信息;遗嘱标志(Will Flag):表示客户端是否需要在与服务器的连接意外断开时发送遗嘱信息;遗嘱QoS(Will QoS):用于设置遗嘱消息的服务质量等级;遗嘱保留(Will Retain):表示遗嘱消息是否需要被服务器保留;用户名标志(Username Flag):表示客户端是否需要发送用户名字段;密码标志(Password Flag):表示客户端是否需要发送密码字段。保持连接(Keep Alive):用于设置心跳包的发送间隔时间,以便客户端和服务器之间保持连接。(2)CONNACK:MQTT连接响应报文CONNACK报文包括固定报头和可变报头两部分。其中,固定报头的第一个字节为0x20,表示这是一个CONNACK报文。可变报头包括了以下字段:连接应答(Connect Acknowledgment):用于表示连接是否成功,一般为0表示成功,其他值表示失败;保留标志(Reserved Flag):保留字段,必须为0。(3)PUBLISH:MQTT发布消息报文PUBLISH报文包括固定报头和可变报头两部分,以及消息体。其中,固定报头的第一个字节由报文类型和QoS级别组合而成,QoS级别可以为0、1或2。可变报头包括了以下字段:主题名(Topic Name):用于标识消息的主题;报文标识符(Packet Identifier):用于在QoS级别为1或2时确认消息分发的情况,如果为0则表示QoS级别为0。消息体包括了要发布的消息内容。(4)PUBACK:MQTT发布确认报文PUBACK报文包括固定报头和可变报头两部分。其中,固定报头的第一个字节为0x40,表示这是一个PUBACK报文。可变报头仅包括一个报文标识符(Packet Identifier)字段,用于确认QoS级别为1的发布消息。(5)PUBREC:MQTT发布接收报文PUBREC报文包括固定报头和可变报头两部分。其中,固定报头的第一个字节为0x50,表示这是一个PUBREC报文。可变报头仅包括一个报文标识符(Packet Identifier)字段,用于确认QoS级别为2的发布消息。(6)PUBREL:MQTT发布释放报文PUBREL报文包括固定报头和可变报头两部分。其中,固定报头的第一个字节为0x62,表示这是一个PUBREL报文。可变报头仅包括一个报文标识符(Packet Identifier)字段,用于确认QoS级别为2的发布消息。(7)PUBCOMP:MQTT发布完成报文PUBCOMP报文包括固定报头和可变报头两部分。其中,固定报头的第一个字节为0x70,表示这是一个PUBCOMP报文。可变报头仅包括一个报文标识符(Packet Identifier)字段,用于确认QoS级别为2的发布消息。(8)SUBSCRIBE:MQTT订阅请求报文SUBSCRIBE报文包括固定报头和可变报头两部分。其中,固定报头的第一个字节为0x82,表示这是一个SUBSCRIBE报文。可变报头包括了以下字段:报文标识符(Packet Identifier):用于确认订阅请求的情况;订阅主题(Subscription Topic):用于设置订阅的主题;服务质量等级(QoS Level):用于设置订阅请求使用的服务质量等级,可以为0、1或2。(9)SUBACK:MQTT订阅确认报文SUBACK报文包括固定报头和可变报头两部分。其中,固定报头的第一个字节为0x90,表示这是一个SUBACK报文。可变报头包括了以下字段:报文标识符(Packet Identifier):用于确认订阅请求的情况;订阅确认等级(Subscription Acknowledgment):用于确认订阅请求的服务质量等级,可以为0、1或2。(10)UNSUBSCRIBE:MQTT取消订阅报文UNSUBSCRIBE报文包括固定报头和可变报头两部分。其中,固定报头的第一个字节为0xA2,表示这是一个UNSUBSCRIBE报文。可变报头包括了以下字段:报文标识符(Packet Identifier):用于确认取消订阅请求的情况;订阅主题(Subscription Topic):用于设置要取消订阅的主题。(11)UNSUBACK:MQTT取消订阅确认报文UNSUBACK报文包括固定报头和可变报头两部分。其中,固定报头的第一个字节为0xB0,表示这是一个UNSUBACK报文。可变报头仅包含报文标识符(Packet Identifier)字段,用于确认取消订阅请求。(12)PINGREQ:MQTT心跳请求报文PINGREQ报文包括固定报头和可变报头两部分。其中,固定报头的第一个字节为0xC0,表示这是一个PINGREQ报文。PINGREQ报文不包含可变报头字段。(13)PINGRESP:MQTT心跳响应报文PINGRESP报文包括固定报头和可变报头两部分。其中,固定报头的第一个字节为0xD0,表示这是一个PINGRESP报文。PINGRESP报文不包含可变报头字段。(14)DISCONNECT:MQTT断开连接报文DISCONNECT报文包括固定报头和可变报头两部分。其中,固定报头的第一个字节为0xE0,表示这是一个DISCONNECT报文。DISCONNECT报文不包含可变报头字段。【3】封装MQTT协议这是一个使用C语言在Linux下建立TCP通信并发送MQTT报文的例子。 根据MQTT报文自己封装协议。 #include <stdio.h>  #include <stdlib.h>  #include <string.h>  #include <sys/socket.h>  #include <arpa/inet.h>  #include <unistd.h>  ​  // 定义MQTT报文类型  #define MQTT_CONNECT   0x10  #define MQTT_CONNACK   0x20  #define MQTT_PUBLISH   0x30  #define MQTT_PUBACK     0x40  #define MQTT_SUBSCRIBE 0x80  #define MQTT_SUBACK     0x90  #define MQTT_UNSUBSCRIBE   0xA0  #define MQTT_UNSUBACK   0xB0  #define MQTT_PINGREQ   0xC0  #define MQTT_PINGRESP   0xD0  #define MQTT_DISCONNECT   0xE0  ​  // 定义MQTT连接标志  #define MQTT_CONNECT_FLAG_CLEAN     0x02  #define MQTT_CONNECT_FLAG_WILL     0x04  #define MQTT_CONNECT_FLAG_WILL_QOS0 0x00  #define MQTT_CONNECT_FLAG_WILL_QOS1 0x08  #define MQTT_CONNECT_FLAG_WILL_QOS2 0x10  #define MQTT_CONNECT_FLAG_WILL_RETAIN   0x20  #define MQTT_CONNECT_FLAG_PASSWORD 0x40  #define MQTT_CONNECT_FLAG_USERNAME 0x80  ​  // 定义MQTT报文结构体  typedef struct mqtt_packet  {   unsigned char *data;   unsigned int length;  }  mqtt_packet_t;  ​  // 建立socket连接并返回socket文件描述符  int socket_connect(char *address, int port)  {   struct sockaddr_in server_address;   int socket_fd = socket(AF_INET, SOCK_STREAM, 0);   if (socket_fd == -1)   {   printf("Failed to create socket!\n");   return -1;   }   server_address.sin_family = AF_INET;   server_address.sin_port = htons(port);   if ((inet_pton(AF_INET, address, &server_address.sin_addr)) <= 0)   {   printf("Invalid address/ Address not supported\n");   return -1;   }   if (connect(socket_fd, (struct sockaddr *)&server_address, sizeof(server_address)) < 0)   {   printf("Connection Failed!\n");   return -1;   }   return socket_fd;  }  // 打包MQTT连接报文  mqtt_packet_t *mqtt_connect(char *client_id, char *username, char *password)  {   mqtt_packet_t *packet = (mqtt_packet_t *)malloc(sizeof(mqtt_packet_t));   unsigned char *data = (unsigned char *)malloc(256);   unsigned int length = 0;   // 固定报头   data[length++] = MQTT_CONNECT;   // 可变报头   data[length++] = 0x0C;   // 清理会话标志和协议版本号   data[length++] = 'M';   data[length++] = 'Q';   data[length++] = 'T';   data[length++] = 'T';   data[length++] = 0x04;   // 协议版本号 // 连接标志   unsigned char flags = MQTT_CONNECT_FLAG_CLEAN;   if (username != NULL)   {   flags |= MQTT_CONNECT_FLAG_USERNAME;   }   if (password != NULL)   {   flags |= MQTT_CONNECT_FLAG_PASSWORD;   }   data[length++] = flags;   data[length++] = 0xFF;   // 保持连接时间低8位   data[length++] = 0xFF;   // 保持连接时间高8位 // 剩余长度   unsigned char remaining_length = length - 1;   data[remaining_length++] = (unsigned char)(length - 2);   packet->data = data;   packet->length = length;   return packet;  }  // 发送MQTT报文  void mqtt_send(int socket_fd, mqtt_packet_t *packet)  {   if (send(socket_fd, packet->data, packet->length, 0) < 0)   {   printf("Failed to send message!\n");   }  }  // 接收MQTT报文  int mqtt_recv(int socket_fd, mqtt_packet_t *packet)  {   unsigned char header[2];   if (recv(socket_fd, header, 2, 0) != 2)   {   printf("Failed to receive message header!\n");   return -1   }   unsigned int remaining_length = 0;   unsigned int multiplier = 1;   int i = 1;   do   {   if (recv(socket_fd, &header[i], 1, 0) != 1)   {   printf("Failed to receive remaining_length byte %d!\n", i);   return -1;   }   remaining_length += (header[i] & 127) * multiplier;   multiplier *= 128;   i++;   }   while ((header[i - 1] & 128) != 0);   packet->length = remaining_length + i;   packet->data = (unsigned char *)malloc(packet->length);   memcpy(packet->data, header, 2);   if (recv(socket_fd, packet->data + 2, packet->length - 2, 0) != packet->length - 2)   {   printf("Failed to receive full message!\n");   return -1;   }   return 0;  }  ​  ​  int main(int argc, char *argv[])  {   // 建立 TCP 连接   int socket_fd = socket_connect("test.mosquitto.org", 1883);   if (socket_fd == -1)   {   printf("Failed to connect to MQTT server!\n");   return -1;   }   printf("Connected to MQTT server!\n");   // 打包并发送 MQTT 连接报文   mqtt_packet_t *connect_packet = mqtt_connect("test_client", NULL, NULL);   mqtt_send(socket_fd, connect_packet);   printf("Sent MQTT CONNECT packet!\n");   free(connect_packet->data);   free(connect_packet);   // 接收 MQTT CONNACK 报文   mqtt_packet_t *connack_packet = (mqtt_packet_t *)malloc(sizeof(mqtt_packet_t));   if (mqtt_recv(socket_fd, connack_packet) != 0)   {   printf("Failed to receive MQTT CONNACK packet!\n");   return -1;   }   if (connack_packet->data[1] != 0x00)   {   printf("MQTT server rejected connection!\n");   return -1;   }   printf("Received MQTT CONNACK packet!\n");   free(connack_packet->data);   free(connack_packet);   // 断开 TCP 连接 close(socket_fd); return 0;  }  
  • [技术干货] STM32单片机上RGB数据转为JPEG格式办法
    【1】项目背景#在STM32单片机上调用OV系列摄像头读取实时视频,然后对数据进行分析,分析之后再通过WIFI或者4G网络传输给服务器保存和显示。因为处理数据时,采用的是RGB源数据格式,处理之后的 数据需要通过网络传输,由于RGB源数据占用内存很大,对接下来的网络传输非常不力,严重影响传输速度。所以,需要先将RGB数据压缩成JPG格式再进行传输。【2】常用的JPGE压缩库(1)libjpeg库libjpeg是一个用于处理JPEG图像格式的库。它提供了一组用于压缩和解压缩JPEG图像的函数,可以在各种操作系统上使用。libjpeg是由Independent JPEG Group开发的自由软件,其主要功能包括压缩和解压缩JPEG图像、转换JPEG图像格式以及进行基本颜色空间转换等。许多图像处理应用程序都使用libjpeg库来实现JPEG图像的读取、写入和处理等功能。(2)TinyJPEG库TinyJPEG是一个用于处理JPEG图像格式的小型库。它是在libjpeg库的基础上进行了简化和优化,以实现更高效的JPEG压缩和解压缩。TinyJPEG库的主要特点是代码量小、易于集成和使用,并且可以在嵌入式设备等资源受限的环境中运行。由于其小巧、快速和可移植性等特点,TinyJPEG库通常用于低功耗设备、嵌入式系统、移动应用程序等领域,以提供高质量的图像处理能力。【3】示例代码TinyJPEG库是一款轻量级的JPEG压缩库,适用于资源受限的嵌入式系统。下面采用STM32F103ZET6作为实验对象,STM32F103ZET6 是一款基于ARM Cortex-M3内核的微控制器,具有高性能和低功耗的特点。将RGB565格式的图像压缩成JPEG格式,需要经过以下几个步骤:将RGB565数据转换为YUV420格式数据。由于JPEG压缩算法基于YUV颜色空间,因此需要先将RGB565数据转换为YUV420格式数据,以便后续处理。对YUV420数据进行预处理。在进行JPEG压缩前,需要对YUV420数据进行预处理,包括分块、离散余弦变换(DCT)、量化等操作。进行霍夫曼编码。将预处理后的数据进行霍夫曼编码,以便能够更好地压缩数据。生成JPEG文件。将编码后的数据写入到JPEG文件中,即可生成JPEG格式的图像数据。以下是一个示例代码,使用TinyJPEG库将RGB565格式的图像压缩成JPEG格式: #include "tiny_jpeg.h"  ​  #define WIDTH   320  #define HEIGHT 240  #define RGB_BUF_SIZE   (WIDTH * HEIGHT * 2)  #define JPEG_BUF_SIZE   (WIDTH * HEIGHT)  ​  uint8_t rgb_buf[RGB_BUF_SIZE];  uint8_t jpeg_buf[JPEG_BUF_SIZE];  ​  int main(void)  {      // 初始化摄像头和LCD等设备  ​      // 获取RGB565格式的图像数据      get_rgb_data(rgb_buf, RGB_BUF_SIZE);  ​      // 将RGB565格式的图像数据转换为YUV420格式数据      uint8_t yuv_buf[WIDTH * HEIGHT * 3 / 2];      rgb_to_yuv(rgb_buf, WIDTH, HEIGHT, yuv_buf);  ​      // 对YUV420格式数据进行预处理      uint8_t dct_buf[JPEG_BUF_SIZE];      preprocess(yuv_buf, WIDTH, HEIGHT, dct_buf);  ​      // 进行霍夫曼编码      int jpeg_size = encode(dct_buf, WIDTH, HEIGHT, jpeg_buf, JPEG_BUF_SIZE);  ​      // 将压缩后的JPEG数据写入到SD卡或其他存储介质中      write_jpeg_to_sd_card(jpeg_buf, jpeg_size);  ​      while (1) {          // 主循环,处理其他任务     }  }TinyJPEG库是一款通用的JPEG压缩库,使用时需要根据具体情况进行修改和适配。在具体实现时,还需要考虑图像大小、压缩比率、编码质量等因素,以便更好地满足实际需求。
  • [技术干货] STM32+DHT11监测环境的温湿度
    【1】DHT11传感器DHT11是一种数字温湿度传感器,能够通过数字信号输出当前环境的温度和湿度值。DHT11可以通过一条数据信号线连接到微控制器或其他外设,从而实现温湿度的实时测量和数据读取。DHT11采用单总线通信协议,只需要连接一个数字信号线和两个电源线,即可实现传感器的数据读取。传感器本身具有一定的温度和湿度校准能力,因此输出的数据比较可靠。DHT11传感器的测量范围为0~50°C的温度和20%~90%的相对湿度,测量精度为±2°C和±5%RH。【2】通信协议DHT11采用单总线通信协议,使用一条数据信号线来传输数据,其中包括起始信号、数据位和校验位。通信协议如下:主机发送一个开始信号给DHT11,即将数据信号线拉低至少18ms以上。主机发出启动信号之后,拉低数据线至少80us,在这个过程中,DHT11将会检测到主机发送的启动信号,并做出回应。DHT11响应主机发出的启动信号后,会拉高数据信号线至少80us,表示传输数据前的“准备工作”已经完成。DHT11开始向主机发送数据,每个数据包包含40个位,高位先传输。在数据传输的过程中,DHT11会将数据信号线从低电平转换为高电平,表示1的开始,持续时间2628us,然后将数据线拉低,表示0的开始,持续时间70us。在发送完40位数据后,DHT11会发送一个校验位。校验位的计算方法是将前四个字节数据相加,求出一个8位校验码,将此校验码与第五个字节进行比较,如果相等,则数据传输成功,否则需要重传数据。主机接收到数据后,需要将数据信号线拉高,以结束传输。【3】读取DHT11温湿度数据以下是一个读取DHT11传感器的温度和湿度示例代码: Copy Code#include "stm32f10x.h"  #include "dht11.h"  ​  #define DHT11_GPIO_PORT GPIOB  #define DHT11_GPIO_PIN GPIO_Pin_12  ​  void delay_us(uint32_t us)  {      us *= (SystemCoreClock / 1000000) / 5;      while (--us);  }  ​  void dht11_start(void)  {      GPIO_InitTypeDef GPIO_InitStruct;  ​      GPIO_InitStruct.GPIO_Pin = DHT11_GPIO_PIN;      GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP;      GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;  ​      GPIO_Init(DHT11_GPIO_PORT, &GPIO_InitStruct);  ​      /* 发送开始信号 */      GPIO_ResetBits(DHT11_GPIO_PORT, DHT11_GPIO_PIN);      delay_us(18000);  ​      GPIO_SetBits(DHT11_GPIO_PORT, DHT11_GPIO_PIN);      delay_us(40);  ​      GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IN_FLOATING;  ​      GPIO_Init(DHT11_GPIO_PORT, &GPIO_InitStruct);  }  ​  uint16_t dht11_read_bit(void)  {      uint16_t retry = 0;  ​      while (GPIO_ReadInputDataBit(DHT11_GPIO_PORT, DHT11_GPIO_PIN) == RESET) {          retry++;          if (retry > 1000) {              return 0;         }          delay_us(1);     }  ​      retry = 0;  ​      while (GPIO_ReadInputDataBit(DHT11_GPIO_PORT, DHT11_GPIO_PIN) == SET) {          retry++;          if (retry > 1000) {              return 0;         }          delay_us(1);     }  ​      if (retry < 30) {          return 0;     } else {          return 1;     }  }  ​  uint8_t dht11_read_byte(void)  {      uint8_t i;      uint8_t data = 0;  ​      for (i = 0; i < 8; i++) {          data <<= 1;          if (dht11_read_bit()) {              data |= 0x01;         }     }  ​      return data;  }  ​  uint8_t dht11_read_data(dht11_data_t *data)  {      uint8_t i;      uint8_t buf[5];      uint8_t checksum = 0;  ​      dht11_start();  ​      if (GPIO_ReadInputDataBit(DHT11_GPIO_PORT, DHT11_GPIO_PIN) == RESET) {          /* 等待DHT11响应 */          while (GPIO_ReadInputDataBit(DHT11_GPIO_PORT, DHT11_GPIO_PIN) == RESET);  ​          /* 等待DHT11发射数据 */          while (GPIO_ReadInputDataBit(DHT11_GPIO_PORT, DHT11_GPIO_PIN) == SET);  ​          /* 接收数据 */          for (i = 0; i < 5; i++) {              buf[i] = dht11_read_byte();         }  ​          /* 校验和 */          checksum = buf[0] + buf[1] + buf[2] + buf[3];  ​          if (checksum == buf[4]) {              data->humidity = buf[0];              data->temperature = buf[2];              return 1;         }     }  ​      return 0;  }  ​  int main(void)  {      dht11_data_t data;  ​      GPIO_InitTypeDef GPIO_InitStruct;  ​      /* 使能GPIOB时钟 */      RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);  ​      /* 配置DHT11引脚为输入模式 */      GPIO_InitStruct.GPIO_Pin = DHT11_GPIO_PIN;      GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IN_FLOATING;      GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;  ​      GPIO_Init(DHT11_GPIO_PORT, &GPIO_InitStruct);  ​      while (1) {          if (dht11_read_data(&data)) {              printf("Temperature: %d°C   Humidity: %d%%\n", data.temperature, data.humidity);         } else {              printf("Error reading data from DHT11.\n");         }          delay_us(2000000);     }  }在这个示例代码中,首先定义了一个dht11_data_t结构体,用于保存读取的温度和湿度数据。然后,编写了一些函数来执行DHT11读取操作。delay_us()函数是一个简单的延迟函数,用于等待一定量的时间。需要精确地计算一个微秒的延迟,并在循环中使用该延迟来等待一段时间。dht11_start()函数用于发送DHT11的开始信号。将DHT11引脚配置为输出模式,并发送18毫秒的低电平信号,然后再发送40微秒的高电平信号。dht11_read_bit()函数用于读取DHT11传输的数据位。等待DHT11输出信号的变化,并根据变化的时间来判断数据位的值。如果一个数据位的响应时间小于30微秒,则被判定为0,否则为1。dht11_read_byte()函数用于读取一个字节的数据(8个数据位)。通过调用dht11_read_bit()函数8次来读取每个数据位,并将结果组合成一个字节。dht11_read_data()函数用于读取整个DHT11数据包,包括温度、湿度和校验和。首先调用dht11_start()函数发送开始信号,然后等待DHT11发送数据。使用dht11_read_byte()函数读取5个字节的数据,并验证校验和以确保数据完整和正确。最后,在main()函数中,初始化GPIO口和DHT11传感器,并执行一个循环来读取数据。如果读取成功,则将温度和湿度打印到串口终端上,否则输出错误信息。
  • [技术干货] STM32通过ADC1读取光敏电阻的值转换光照强度
    【1】光敏电阻的原理光敏电阻是一种半导体元件,它的电阻值会随着照射在其表面的光线强度的变化而发生改变。当光线越强,光敏电阻的电阻值就越小;当光线较弱或没有光照射时,电阻值就会增大。光敏电阻广泛应用于光电控制、光度计、自动调节亮度灯等领域。常见的光敏电阻有硫化镉(CdS)光敏电阻和硒化铟(InSb)光敏电阻等。与其他传感器相比,光敏电阻具有以下优点:灵敏度高:对光线强度的变化非常敏感。响应速度快:一般情况下响应时间只需几毫秒。易于集成:小巧轻便,易于安装和集成到各种设备中。价格低廉:相对于其他光电传感器,光敏电阻的价格较为低廉。但是,光敏电阻也有其缺点。由于光敏电阻本身的特性,其输出不太稳定,精度较低,并且受环境光线干扰较大。因此,在实际应用中,需要根据具体情况进行选择并对其输出信号进行适当的处理和滤波才能得到准确的测量结果。【2】STM32采集光敏电阻值的代码以下是一个基于STM32F103C8T6和光敏电阻的示例代码,它可以采集光敏电阻的数据并通过串口打印出来。请注意,此示例使用了HAL库和CubeMX配置工具。 cCopy Code#include "main.h"  #include "stdio.h"  #include "string.h"  ​  ADC_HandleTypeDef hadc1;  UART_HandleTypeDef huart1;  ​  float LightIntensity;  ​  int main(void)  {    HAL_Init();    SystemClock_Config();    MX_GPIO_Init();    MX_ADC1_Init();    MX_USART1_UART_Init();  ​    while (1)   {      // 启动ADC转换      HAL_ADC_Start(&hadc1);      // 等待转换完成      HAL_ADC_PollForConversion(&hadc1, 100);      // 获取ADC转换结果      uint16_t adc_value = HAL_ADC_GetValue(&hadc1);  ​      // 将ADC转换结果转换为光线强度      LightIntensity = (float)adc_value / 4095 * 100;  ​      // 将数据打印到串口      char msg[50];      sprintf(msg, "Light intensity: %.2f%%\n", LightIntensity);      HAL_UART_Transmit(&huart1, (uint8_t*)msg, strlen(msg), 1000);  ​      // 延迟一段时间再次采集      HAL_Delay(5000);   }  }  ​  void SystemClock_Config(void)  {    RCC_OscInitTypeDef RCC_OscInitStruct = {0};    RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};  ​    /** Configure the main internal regulator output voltage    */    __HAL_RCC_PWR_CLK_ENABLE();    __HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_VOLTAGE_SCALE1);    /** Initializes the RCC Oscillators according to the specified parameters    * in the RCC_OscInitTypeDef structure.    */    RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;    RCC_OscInitStruct.HSEState = RCC_HSE_ON;    RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1;    RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;    RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;    RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9;    if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)   {      Error_Handler();   }    /** Initializes the CPU, AHB and APB buses clocks    */    RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK                                |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;    RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;    RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;    RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2;    RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;  ​    if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK)   {      Error_Handler();   }  }  ​  static void MX_ADC1_Init(void)  {  ​    ADC_ChannelConfTypeDef sConfig = {0};  ​    /** Common config    */    hadc1.Instance = ADC1;    hadc1.Init.ScanConvMode = DISABLE;    hadc1.Init.ContinuousConvMode = ENABLE;    hadc1.Init.DiscontinuousConvMode = DISABLE;    hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START;    hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT;    hadc1.Init.NbrOfConversion = 1;    if (HAL_ADC_Init(&hadc1) != HAL_OK)   {      Error_Handler();   }    /** Configure Regular Channel    */    sConfig.Channel = ADC_CHANNEL_1;    sConfig.Rank = ADC_REGULAR_RANK_1;    sConfig.SamplingTime = ADC_SAMPLETIME_13CYCLES_5;    if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK)   {      Error_Handler();   }  ​  }  ​  static void MX_USART1_UART_Init(void)  {  ​    huart1.Instance = USART1;    huart1.Init.BaudRate = 115200;    huart1.Init.WordLength = UART_WORDLENGTH_8B;    huart1.Init.StopBits = UART_STOPBITS_1;    huart1.Init.Parity = UART_PARITY_NONE;    huart1.Init.Mode = UART_MODE_TX_RX;    huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;    huart1.Init.OverSampling = UART_OVERSAMPLING_16; if (HAL_UART_Init(&huart1) != HAL_OK) { Error_Handler(); }  ​  }  ​  static void MX_GPIO_Init(void)  {   GPIO_InitTypeDef GPIO_InitStruct = {0};   /* GPIO Ports Clock Enable */   __HAL_RCC_GPIOA_CLK_ENABLE();   /*Configure GPIO pin : PA1 */   GPIO_InitStruct.Pin = GPIO_PIN_1;   GPIO_InitStruct.Mode = GPIO_MODE_ANALOG;   HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);  }  void Error_Handler(void)  {   while(1);  }  #ifdef USE_FULL_ASSERT  void assert_failed(uint8_t *file, uint32_t line)  {  }  #endif在此代码中,PA1被配置成了模拟输入通道,并在ADC采样时使用。通过将采集到的ADC值转换为光线强度并打印出来,可以实现对光敏电阻的测量。
  • [技术干货] STM32读取MQ2烟雾浓度数据判断烟雾是否超标
    【1】MQ2传感器是什么?MQ2传感器是一种可探测多种气体的传感器,常用于监测烟雾、液化气、丙酮、乙醇、甲醛、天然气等有害气体。MQ2传感器基于半导体敏感元件,通过检测气体中有害物质的浓度变化来实现气体检测。MQ2传感器具有以下特点:可靠性高:采用优质半导体敏感元件,响应速度快、灵敏度高。响应时间快:在检测到有害气体时能够立即发出警报。易于集成:小巧轻便,易于安装和集成到各种设备中。价格低廉:相对于其他气体检测传感器,MQ2传感器的价格较为低廉。MQ2传感器广泛应用于家庭、工业、医疗、环保等领域,帮助人们实时监测气体浓度,保障生命健康和财产安全。【2】MQ2传感器浓度如何转换?MQ2传感器的电压输出值可以通过ADC进行采集。MQ2传感器检测到烟雾等有害气体时,其敏感材料的电阻值会发生变化,从而导致输出电压值的变化。因此,可以通过采集MQ2传感器的输出电压值来判断烟雾浓度。MQ2传感器的输出电压与烟雾浓度之间的关系是线性的,需要进行一定的转换才能得出准确的烟雾浓度。常见的转换方法如下:(1)标定法将MQ2传感器置于标准烟雾环境中,记录其输出电压值和对应的烟雾浓度,并建立二者之间的关系模型。然后再使用这个模型将采集到的MQ2传感器输出电压值转换为相应的烟雾浓度。该方法测量精度较高,但需要专业仪器作为标准烟雾环境。(2)经验公式法根据经验统计,MQ2传感器的电压输出值与实际烟雾浓度之间呈现出某种函数关系。通过实验数据拟合出该函数关系,就可以将MQ2传感器的电压输出值直接转换为烟雾浓度。该方法需要进行多次实验,并对数据进行处理和拟合,相对较为复杂。(3)查表法通过实验得到一系列MQ2传感器输出电压值与对应烟雾浓度的关系数据,形成一张转换表格。在实际使用过程中,将采集到的MQ2传感器输出电压值查表后即可得到相应的烟雾浓度。该方法简单易行,但需要大量实验数据作为基础。【3】STM32采集MQ2烟雾浓度以下是一个基于STM32F103C8T6和MQ2传感器的示例代码,它可以采集MQ2的烟雾浓度并通过串口打印出来。请注意,此示例使用了HAL库和CubeMX配置工具。 #include "main.h"  #include "stdio.h"  #include "string.h"  ​  ADC_HandleTypeDef hadc1;  UART_HandleTypeDef huart1;  ​  void SystemClock_Config(void);  static void MX_GPIO_Init(void);  static void MX_ADC1_Init(void);  static void MX_USART1_UART_Init(void);  ​  float SmokeDensity;  ​  int main(void)  {    HAL_Init();    SystemClock_Config();    MX_GPIO_Init();    MX_ADC1_Init();    MX_USART1_UART_Init();  ​    while (1)   {      // 启动ADC转换      HAL_ADC_Start(&hadc1);      // 等待转换完成      HAL_ADC_PollForConversion(&hadc1, 100);      // 获取ADC转换结果      uint16_t adc_value = HAL_ADC_GetValue(&hadc1);  ​      // 将ADC转换结果转换为烟雾浓度      SmokeDensity = (float)adc_value / 4095 * 100;  ​      // 将数据打印到串口      char msg[50];      sprintf(msg, "Smoke density: %.2f%%\n", SmokeDensity);      HAL_UART_Transmit(&huart1, (uint8_t*)msg, strlen(msg), 1000);  ​      // 延迟一段时间再次采集      HAL_Delay(5000);   }  }  ​  void SystemClock_Config(void)  {    RCC_OscInitTypeDef RCC_OscInitStruct = {0};    RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};  ​    /** Configure the main internal regulator output voltage    */    __HAL_RCC_PWR_CLK_ENABLE();    __HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_VOLTAGE_SCALE1);    /** Initializes the RCC Oscillators according to the specified parameters    * in the RCC_OscInitTypeDef structure.    */    RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;    RCC_OscInitStruct.HSEState = RCC_HSE_ON;    RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1;    RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;    RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;    RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9;    if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)   {      Error_Handler();   }    /** Initializes the CPU, AHB and APB buses clocks    */    RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK                                |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;    RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;    RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;    RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2;    RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;  ​    if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK)   {      Error_Handler();   }  }  ​  static void MX_ADC1_Init(void)  {   ADC_ChannelConfTypeDef sConfig = {0};   /** Common config    */   hadc1.Instance = ADC1;   hadc1.Init.ScanConvMode = DISABLE;   hadc1.Init.ContinuousConvMode = ENABLE;   hadc1.Init.DiscontinuousConvMode = DISABLE;   hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START;   hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT;   hadc1.Init.NbrOfConversion = 1;   if (HAL_ADC_Init(&hadc1) != HAL_OK)   {   Error_Handler();   }   /** Configure Regular Channel    */   sConfig.Channel = ADC_CHANNEL_1;   sConfig.Rank = ADC_REGULAR_RANK_1;   sConfig.SamplingTime = ADC_SAMPLETIME_13CYCLES_5;   if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK)   {   Error_Handler();   }  }  static void MX_USART1_UART_Init(void)  {   huart1.Instance = USART1;   huart1.Init.BaudRate = 115200;   huart1.Init.WordLength = UART_WORDLENGTH_8B;   huart1.Init.StopBits = UART_STOPBITS_1;   huart1.Init   .Parity = UART_PARITY_NONE;   huart1.Init.Mode = UART_MODE_TX_RX;   huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;   huart1.Init.OverSampling = UART_OVERSAMPLING_16;   if (HAL_UART_Init(&huart1) != HAL_OK)   {   Error_Handler();   }  }  static void MX_GPIO_Init(void)  {   GPIO_InitTypeDef GPIO_InitStruct = {0};   /* GPIO Ports Clock Enable */   __HAL_RCC_GPIOA_CLK_ENABLE();   /*Configure GPIO pin : PA1 */   GPIO_InitStruct.Pin = GPIO_PIN_1;   GPIO_InitStruct.Mode = GPIO_MODE_ANALOG;   HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);  }  void Error_Handler(void)  {   while(1);  }  #ifdef USE_FULL_ASSERT  void assert_failed(uint8_t *file, uint32_t line)  {  }  #endif在此示例代码中,PA1被配置成了模拟输入通道,并在ADC采样时使用。通过将采集到的ADC值转换为烟雾浓度并打印出来,可以实现对MQ2传感器的烟雾检测。
  • [技术干货] STM32读取BH1750光照强度数据打印到串口
    【1】BH1750是什么?BH1750是一种数字式环境光强度传感器(Digital Light Sensor),也称为其他名称,例如GY-302传感器、BH1750FVI传感器等。它的工作原理是通过收集光线照射到传感器上的量来测量环境亮度。使用I2C(Inter-Integrated Circuit)接口,BH1750可以轻松地接入到各种嵌入式系统中,并提供实时的环境光强度数据。其度量范围是1-65535 lux,测量精度可以达到每个范围16位,使其成为许多应用中的理想选择。例如,自动照明控制、日光灯节能控制、智能家居、汽车照明系统和摄影中的曝光控制等等。BH1750还有一些其他优点。例如,其本身具有非常低的功率消耗(例如小于1μA),这意味着它可以轻松地集成在嵌入式系统中,并且非常适用于电池供电的系统。并且它是一种数字式光强度传感器,相比于模拟式光强度传感器,它的抗干扰性能更好,并且可以一次完成多种测量,例如高分辨率的光强测量、低光强测量等。【2】什么是IIC协议?IIC(Inter-Integrated Circuit)协议也称为I2C协议,是一种串行通信协议,由Philips公司(现在的NXP公司)于1980年代初期开发。它是一种双向、两线式的串行通信协议,通常被用于板间通信以及嵌入式系统中的设备之间的通信。IIC协议由两根线构成:数据线(SDA)和时钟线(SCL)。SDA线负责传输数据,而SCL线则负责传输时钟信号。在IIC总线上,多个设备可以连接到同一根时钟线和数据线上,通过设置每个设备的唯一地址来进行通信。IIC协议支持多主机和多从机的通信,也支持多种通信速率(通常为100kHz或400kHz)。IIC协议是一种简单易用的通信协议,因此被广泛应用于各种嵌入式系统和电子设备中,例如SMBus、PMbus、I2C EEPROM、I2C LCD、I2C ADC、I2C DAC、I2C RTC等等。【2】STM32读取BH1750数据下面是使用标准库函数STM32F103C8T6读取BH1750光照强度数据并打印到串口的代码: #include "main.h"  #include "stm32f1xx_hal.h"  #include "stdio.h"  ​  #define BH1750_ADDRESS 0x23 //BH1750地址  ​  I2C_HandleTypeDef hi2c1;  //IIC外设句柄  ​  void SystemClock_Config(void);  static void MX_GPIO_Init(void);  static void MX_USART1_UART_Init(void);  static void MX_I2C1_Init(void);  ​  int main(void)  {    HAL_Init();    SystemClock_Config();    MX_GPIO_Init();    MX_USART1_UART_Init();    MX_I2C1_Init();  ​    uint8_t buf[2];    uint16_t value = 0;  ​    //初始化BH1750    buf[0] = 0x01; //使用高分辨率模式    HAL_I2C_Master_Transmit(&hi2c1, BH1750_ADDRESS << 1, buf, 1, 100);    HAL_Delay(100);  ​    while (1)   {      //读取光照强度数据      buf[0] = 0x00; //高8位      HAL_I2C_Master_Transmit(&hi2c1, BH1750_ADDRESS << 1, buf, 1, 100);      HAL_I2C_Master_Receive(&hi2c1, BH1750_ADDRESS << 1, buf, 2, 100);  ​      value = (buf[0] << 8) | buf[1];      value = value/1.2; //单位转换,参考BH1750手册      printf("Light intensity: %d lux\n", value);            HAL_Delay(1000);   }  }  ​  void SystemClock_Config(void)  {   RCC_OscInitTypeDef RCC_OscInitStruct = {0};   RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};   RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSI;   RCC_OscInitStruct.HSIState = RCC_HSI_ON;   RCC_OscInitStruct.HSICalibrationValue = RCC_HSICALIBRATION_DEFAULT;   RCC_OscInitStruct.PLL.PLLState = RCC_PLL_NONE;   if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)   {   Error_Handler();   }   RCC_ClkInitStruct.ClockType = RCC   static void MX_GPIO_Init(void)   {   GPIO_InitTypeDef GPIO_InitStruct = {0};   __HAL_RCC_GPIOA_CLK_ENABLE();   /*Configure GPIO pin Output Level */   HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1, GPIO_PIN_RESET);   /*Configure GPIO pin : PA1 */   GPIO_InitStruct.Pin = GPIO_PIN_1;   GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;   GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;   HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);   }   static void MX_I2C1_Init(void)   {   hi2c1.Instance = I2C1;   hi2c1.Init.ClockSpeed = 100000;   hi2c1.Init.DutyCycle = I2C_DUTYCYCLE_2;   hi2c1.Init.OwnAddress1 = 0;   hi2c1.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT;   hi2c1.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE;   hi2c1.Init.OwnAddress2 = 0;   hi2c1.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE;   hi2c1.Init.NoStretchMode = I2C_NOSTRETCH_DISABLE;   if (HAL_I2C_Init(&hi2c1) != HAL_OK)   {   Error_Handler();   }   }   void MX_USART1_UART_Init(void)   {   huart1.Instance = USART1;   huart1.Init.BaudRate = 115200;   huart1.Init.WordLength = UART_WORDLENGTH_8B;   huart1.Init.StopBits = UART_STOPBITS_1;   huart1.Init.Parity = UART_PARITY_NONE;   huart1.Init.Mode = UART_MODE_TX_RX;   huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;   huart1.Init.OverSampling = UART_OVERSAMPLING_16;   if (HAL_UART_Init(&huart1) != HAL_OK)   {   Error_Handler();   }   }   void Error_Handler(void)   {   __disable_irq();   while (1)   {   }   }在代码中,使用HAL库函数初始化了I2C接口和USART串口,同时使用了STM32提供的延时库函数HAL_Delay()。在主函数中,首先定义了一个缓冲区buf[2]和一个变量value,缓冲区buf[2]用于存储从BH1750读取的光照强度数据。变量value用于存储经过单位转换后的光照强度值。接着,使用HAL_I2C_Master_Transmit()函数向BH1750传输一个命令,以初始化BH1750。在这里,将BH1750设置为使用高分辨率模式,以获得更高的测量精度。紧接着,使用HAL_Delay()函数延时100毫秒,以确保BH1750设备初始化成功。然后,在while循环中,使用HAL_I2C_Master_Transmit()和HAL_I2C_Master_Receive()函数从BH1750读取光照强度数据。读取的光照强度值存储在缓冲区buf[2]中,并进行了单位转换,最后通过printf()函数打印到串口。在此示例中,使用了printf()函数将光照强度值打印到串口,因此需要在调试器中打开串口窗口才能看到打印的数据。为了使代码正常工作,应在stm32f1xx_hal_conf.h头文件中将USE_HAL_DRIVER宏定义设置为1。
  • [技术干货] ESP8266调用NTP服务器进行时间校准
    一、背景知识【1】什么是NTP服务器?NTP是网络时间协议(Network Time Protocol,简称NTP),是一种用于同步计算机时间的协议。NTP服务器指的是提供NTP服务的计算机或设备。NTP服务器的主要功能是保证网络上的所有设备的时间同步,以确保各个设备相互之间的时间协调一致。NTP服务器通常连接到具有高度精确时间源的设备,例如:GPS接收器或原子钟,以确保提供准确如一的时间。网络上的计算机可以通过连接到NTP服务器来同步其时间,并确保它们在同一时刻进行操作。目前有许多可以使用的NTP服务器,以下是一些常用的NTP服务器列表: 1. cn.ntp.org.cn  2. ntp.sjtu.edu.cn  3. ntp.linux.org.cn  4. time.nist.gov.cn  5. ntp.aliyun.com  6. ntp.api.bz  7. ntp1.aliyun.com  8. time1.cloud.tencent.com  9. pool.ntp.org.cn【2】RTC实时时钟是什么?RTC (Real-Time Clock)实时时钟,是指一种专门用于记忆日期、时间的计时芯片或模块。一般包括一个时钟芯片、一块石英晶体、一块温度补偿电路、电源管理电路等组成。RTC可以精确地记录日期和时间,即使是在断电等异常情况下,也能保持记录的时间长达数年。常常用于嵌入式系统、数据采集设备等领域,是一种至关重要的设备。在某些系统应用中,RTC也会成为其他设备的时钟源,如单片机或微控制器单位等。RTC的时间精度通常为ppm 级别,即每百万分之一,能够满足大多数实时应用场景的要求。为了提高RTC的稳定度和精度,许多RTC都带有自动校正功能,可以自动从外部时钟源或NTP服务器中获取准确的时间,并进行校正。同时,许多RTC还会集成电源管理功能,支持低功耗模式以延长电池寿命。二、ESP8266获取网络时间要通过ESP8266联网并获取网络时间,需要执行以下步骤:在STM32F103ZET6上配置UART串口以与ESP8266进行通信。使用AT指令将ESP8266连接到Wi-Fi网络。可以使用以下指令: AT+CWJAP="SSID","password"其中,替换 "SSID" 为自己的Wi-Fi网络名称,"password" 是Wi-Fi密码。使用AT指令连接到NTP服务器并获取时间。您可以使用以下指令: AT+CIPSNTPCFG=0,1,"pool.ntp.org"  AT+CIPSNTPTIME?这将连接到ntp服务器并检索当前的UTC时间。将ESP8266返回的UTC时间转换为本地时间。您需要知道您所在的时区,并对UTC进行适当的调整。将本地时间设置为STM32F103ZET6上的RTC实时时钟。下面是一个示例代码 #include <stdio.h>  #include "stm32f10x.h"  ​  // UART配置  void uart_init() {    RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);  ​      USART_InitTypeDef USART_InitStructure;      USART_InitStructure.USART_BaudRate = 115200;      USART_InitStructure.USART_WordLength = USART_WordLength_8b;      USART_InitStructure.USART_StopBits = USART_StopBits_1;      USART_InitStructure.USART_Parity = USART_Parity_No;      USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;      USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;      USART_Init(USART1, &USART_InitStructure);  ​      USART_Cmd(USART1, ENABLE);  }  ​  // 发送AT指令并等待响应  int send_at_command(char* command, char* response, uint32_t timeout) {    // 发送命令    USART_SendData(USART1, (uint8_t*)command, strlen(command));        // 等待响应    uint32_t start_time = HAL_GetTick();    while ((HAL_GetTick() - start_time) < timeout) {      if (USART_GetFlagStatus(USART1, USART_FLAG_RXNE) != RESET) {        char c = USART_ReceiveData(USART1);                // 检查是否收到了预期的响应        if (strstr(response, c) != NULL) {          return 0; // 成功       }     }   }        return -1; // 超时或没有收到预期的响应  }  ​  // 连接ESP8266到Wi-Fi  void connect_to_wifi() {    char command[50];    char response[100];        // 设置Wi-Fi SSID和密码    sprintf(command, "AT+CWJAP="%s","%s"\r\n", "YourSSID", "YourPassword");    send_at_command(command, "OK", 5000);  }  ​  // 连接到NTP服务器并获取时间  int get_ntp_time(uint32_t* time) {    char response[100];        // 配置SNTP客户端    send_at_command("AT+CIPSNTPCFG=0,1,"pool.ntp.org"\r\n", "OK", 5000);        // 获取时间    send_at_command("AT+CIPSNTPTIME?\r\n", response, 5000);        // 解析响应并提取时间戳    char* token = strtok(response, ",");    uint32_t timestamp = atoi(token);    *time = timestamp - 2208988800UL; // 转换为Unix时间戳        return 0;  }  ​  // 将时间设置到RTC  void set_rtc_time(uint32_t time) {    // 启用PWR和BKP外设时钟    RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE);        // 解锁备份寄存器区域    PWR_BackupAccessCmd(ENABLE);        // 配置RTC    RCC_RTCCLKConfig(RCC_RTCCLKSource_HSE_Div128); // RTC时钟源为HSE/128    RCC_RTCCLKCmd(ENABLE); // 启用RTC时钟        RTC_InitTypeDef RTC_InitStructure;    // 配置RTC时钟      RTC_InitStructure.RTC_HourFormat = RTC_HourFormat_24; RTC_InitStructure.RTC_AsynchPrediv = 127;      RTC_InitStructure.RTC_SynchPrediv = 255;      RTC_Init(&RTC_InitStructure);  ​  // 设置RTC时间      RTC_TimeTypeDef RTC_TimeStruct;      RTC_DateTypeDef RTC_DateStruct;  ​  // 将Unix时间戳转换为RTC时间和日期    uint32_t days = time / 86400;      uint32_t seconds = time % 86400;      uint32_t hours = seconds / 3600;      uint32_t minutes = (seconds % 3600) / 60;      uint32_t secs = (seconds % 3600) % 60;      uint32_t year = 1970;      uint32_t month = 1;      while (days > 365) { if ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0) { days -= 366; } else { days -= 365; } year++; }      while (days > 0) { if (month == 2)     { if ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0) { if (days > 29) { days -= 29; } else { break; } } else { if (days > 28) { days -= 28; } else { break; } } } else if (month == 4 || month == 6 || month == 9 || month == 11) { if (days > 30) { days -= 30; } else { break; } } else { if (days > 31) { days -= 31; } else { break; } } month++; if (month > 12) { month = 1; year++; } }  ​  RTC_TimeStruct.RTC_Hours = hours; RTC_TimeStruct.RTC_Minutes = minutes; RTC_TimeStruct.RTC_Seconds = secs; RTC_DateStruct.RTC_Date = days; RTC_DateStruct.RTC_Month = month; RTC_DateStruct.RTC_Year = year - 2000;  ​  // 设置RTC时间和日期      RTC_SetTime(RTC_Format_BIN, &RTC_TimeStruct);      RTC_SetDate(RTC_Format_BIN, &RTC_DateStruct); }  ​  int main()  {      // 初始化UART串口      uart_init();  ​     // 连接ESP8266到Wi-Fi      connect_to_wifi();  ​  // 获取NTP时间      uint32_t ntp_time; get_ntp_time(&ntp_time);  ​  // 将时间设置到      RTC set_rtc_time(ntp_time);  ​  while (1) { // 做其他的事情... } }
  • [技术干货] ESP8266获取天气预报信息,并使用CJSON解析天气预报数据
    一、实现功能当前文章介绍如何使用ESP8266和STM32微控制器,搭配OLED显示屏,制作一个能够实时显示天气预报的智能设备。将使用心知天气API来获取天气数据,并使用MQTT协议将数据传递给STM32控制器,最终在OLED显示屏上显示。心知天气是一家专业的气象数据服务提供商,致力于为全球用户提供高质量、定制化的气象数据服务。其主要产品包括天气API、空气质量API、灾害预警API等。用户可以通过心知天气的API接口,获取准确、实时的天气数据,从而为各种应用场景提供支持,例如智能家居、出行、电商等。心知天气的数据覆盖全球200多个国家和地区,每日处理超过10亿次API请求,是业内领先的气象数据服务提供商之一。二、硬件准备1. ESP8266模块ESP8266是一款WiFi模块,它具有强大的网络连接功能,可以轻松地连接到互联网。将使用ESP8266模块来获取天气数据,并将其发送给STM32控制器。具体来说,我们将使用正点原子ATK-ESP8266模块,这是一款集成ESP8266芯片的小板子。2. STM32微控制器STM32是一款强大的32位微控制器,具有多种接口和功能。将使用STM32F103C8T6控制器,这是一款非常流行的型号,易于获得且价格较为合理。3. OLED显示屏OLED是一种非常流行的显示技术,具有高对比度、低功耗、快速响应等优点。将使用0.96英寸128x64像素的OLED显示屏。三、CJSON解析天气预报数据3.1 接口返回的数据 {    "results": [     {        "location": {          "id": "WTEMH46Z5N09",          "name": "合肥",          "country": "CN",          "path": "合肥,合肥,安徽,中国",          "timezone": "Asia/Shanghai",          "timezone_offset": "+08:00"       },        "now": {          "text": "阴",          "code": "9",          "temperature": "12",          "feels_like": "18",          "pressure": "1000",          "humidity": "89",          "visibility": "12.0",          "wind_direction": "西南",          "wind_direction_degree": "245",          "wind_speed": "19.0",          "wind_scale": "3",          "clouds": "85",          "dew_point": ""       },        "last_update": "2023-04-04T14:20:13+08:00"     }   ]  }3.2 CJSON是什么CJSON是一款轻量级的C语言JSON解析器,其全称是“cJSON”,由Dave Gamble编写。它简单易用,可嵌入到C应用程序中,既支持JSON字符串的解析,也支持JSON对象的创建及操作。CJSON不依赖于任何其他的库或组件,使用它只需要引入其头文件即可。CJSON的使用方式相对来说比较简单,需要进行以下几个步骤: 1. 在应用程序中包含cJSON的头文件:#include "cJSON.h"。  2. 调用cJSON_Parse函数,将JSON字符串转换为CJSON对象。  3. 使用cJSON提供的API函数对CJSON对象进行操作,包括读取、修改、删除、添加等。  4. 在程序结束时,记得释放cJSON对象的内存空间,避免内存泄漏。CJSON的解析速度相对较快,占用的内存开销也比较小,因此非常适用于资源有限的嵌入式系统中使用。3.3 解析数据使用CJSON解析上述JSON数据非常简单,只需要按照以下步骤操作:引入CJSON库文件 cCopy Code#include <cJSON.h>解析JSON数据并创建cJSON对象 cCopy Codechar* json_data = "{"results":[{"location":{"id":"WTEMH46Z5N09","name":"合肥","country":"CN","path":"合肥,合肥,安徽,中国","timezone":"Asia/Shanghai","timezone_offset":"+08:00"},"now":{"text":"阴","code":"9","temperature":"12","feels_like":"18","pressure":"1000","humidity":"89","visibility":"12.0","wind_direction":"西南","wind_direction_degree":"245","wind_speed":"19.0","wind_scale":"3","clouds":"85","dew_point":""},"last_update":"2023-04-04T14:20:13+08:00"}]}";  cJSON* root = cJSON_Parse(json_data);在这个代码片段中,我们首先定义了一个字符串类型的变量json_data,用于存储上述JSON数据。然后,我们调用cJSON_Parse()函数来解析JSON数据,并将解析结果保存在root指针所指向的cJSON对象中。从cJSON对象中提取数据 cCopy CodecJSON* location = cJSON_GetObjectItem(root, "location");  char* city = cJSON_GetObjectItem(location, "name")->valuestring;  cJSON* now = cJSON_GetObjectItem(root, "now");  int temperature = cJSON_GetObjectItem(now, "temperature")->valueint;  char* text = cJSON_GetObjectItem(now, "text")->valuestring;在这个代码片段中,我们使用cJSON_GetObjectItem()函数从root指针所指向的cJSON对象中提取一个名为location的JSON对象,并从该JSON对象中获取名为name的字符串类型变量。类似地,我们也可以从root指针所指向的cJSON对象中提取名为now的JSON对象,并从该JSON对象中获取名为temperature和text的整型和字符串类型变量。释放cJSON对象 cCopy CodecJSON_Delete(root);最后,我们需要释放之前创建的cJSON对象,以释放内存空间。完整的代码示例如下: cCopy Code#include <cJSON.h>  #include <stdio.h>  ​  int main() {      char* json_data = "{"results":[{"location":{"id":"WTEMH46Z5N09","name":"合肥","country":"CN","path":"合肥,合肥,安徽,中国","timezone":"Asia/Shanghai","timezone_offset":"+08:00"},"now":{"text":"阴","code":"9","temperature":"12","feels_like":"18","pressure":"1000","humidity":"89","visibility":"12.0","wind_direction":"西南","wind_direction_degree":"245","wind_speed":"19.0","wind_scale":"3","clouds":"85","dew_point":""},"last_update":"2023-04-04T14:20:13+08:00"}]}";      cJSON* root = cJSON_Parse(json_data);  ​      cJSON* location = cJSON_GetObjectItem(root, "location");      char* city = cJSON_GetObjectItem(location, "name")->valuestring;      cJSON* now = cJSON_GetObjectItem(root, "now");      int temperature = cJSON_GetObjectItem(now, "temperature")->valueint;      char* text = cJSON_GetObjectItem(now, "text")->valuestring;  ​      printf("City: %s\n", city);      printf("Temperature: %d\n", temperature);      printf("Weather: %s\n", text);  ​      cJSON_Delete(root);  ​      return 0;  }在这个代码示例中,使用了cJSON_Parse()、cJSON_GetObjectItem()、cJSON_Delete()等函数来解析和处理JSON数据。3.4 获取数据下面是ESP8266访问HTTP接口请求的代码: #include <SoftwareSerial.h>  ​  // 定义ESP8266串口对象  SoftwareSerial esp8266(PA10, PA9); // RX, TX  ​  void setup() {    Serial.begin(9600);  ​    // 初始化ESP8266串口通信波特率为9600    esp8266.begin(9600);  ​    // 发送AT指令测试ESP8266是否正常工作    esp8266.println("AT");    delay(500);    if (esp8266.find("OK")) {      Serial.println("ESP8266 is working properly.");   } else {      Serial.println("ESP8266 is not working properly.");   }  }  ​  void loop() {    // 向ESP8266发送HTTP请求    esp8266.println("AT+CIPSTART="TCP","api.seniverse.com",80");    if (esp8266.find("OK")) {      Serial.println("TCP connection established.");   } else {      Serial.println("TCP connection failed.");   }  ​    String url = "/v3/weather/now.json?key=your_API_KEY&location=your_LOCATION";    String request = "GET " + url + " HTTP/1.1\r\n" +                     "Host: api.seniverse.com\r\n" +                     "User-Agent: STM32/1.0\r\n" +                     "Connection: close\r\n\r\n";    int length = request.length();    String cmd = "AT+CIPSEND=" + String(length);    esp8266.println(cmd);    if (esp8266.find(">")) {      Serial.println("Sending HTTP request...");      esp8266.print(request);   } else {      Serial.println("Failed to send HTTP request.");   }  ​    // 接收HTTP响应    while (esp8266.available()) {      String response = esp8266.readStringUntil('\n');      Serial.println(response);   }  ​    // 关闭TCP连接    esp8266.println("AT+CIPCLOSE");    delay(1000);  }在这个示例代码中,初始化了ESP8266串口对象,并通过发送AT指令测试ESP8266是否正常工作。然后,在loop()函数中,向ESP8266发送一个HTTP请求,包括请求头和请求体。发送完毕后,等待ESP8266返回HTTP响应并将其打印出来。最后,关闭TCP连接并等待一秒钟,然后重复上述步骤。
  • [技术干货] 基于STM32+RC522设计的门禁系统
    一、项目背景门禁系统是现代社会中非常重要的安全控制系统之一,其功能是在保障建筑物安全的同时,为合法用户提供便利。当前设计一种基于STM32+RC522的门禁系统设计方案,通过RFID-RC522模块实现了对用户卡的注册、识别及身份验证,通过控制SG90舵机实现门锁的开关,具有较高的安全性和可靠性。实验结果表明,该门禁系统可以有效地保障建筑物的安全性。门禁系统广泛应用于各种建筑物、企事业单位,用于管理人员的进出、控制人员活动范围、实现安全监控等功能。传统的门禁系统采用密码输入或刷卡的方式进行身份验证,但存在易被破解的风险。基于RFID的门禁系统已经成为一种相对先进的安全控制方案。本次设计的STM32+RC522门禁系统,通过RFID-RC522模块对用户的卡进行注册、识别完成身份识别,对门锁进行开关。系统带了OLED显示屏,输入用户密码登录之后,可以对新卡片进行注册,添加新卡片,对不使用的卡片进行注销。在系统里,IC卡的数据都存储在卡的内部扇区里,通过卡的内部空间进行管理。采用5V-步进电机的版本:二、系统设计门禁系统由STM32F103C8T6单片机、RFID-RC522模块、SG90舵机、LCD1602液晶显示屏、键盘模块等组成。其中,STM32F103C8T6单片机作为系统的核心控制器,控制程序的执行;RFID-RC522模块作为识别用户卡片的设备;SG90舵机作为门锁控制设备;OLED显示屏提供用户输入信息和系统信息的显示;键盘模块方便用户进行密码和卡片信息的输入。2.1 软件设计【1】RFID卡信息管理本系统采用卡的内部空间进行IC卡信息的管理。每个IC卡可以分为多个扇区,每个扇区包含多个块,每个块包含16个字节。扇区0是厂家已经预留好的,用于存储卡片的序列号,扇区1-15可以由用户自己配置,用于存储一些私有数据,如用户身份、车牌号、员工编号等。在本系统中,IC卡信息的管理主要包括三个方面:新卡片注册、卡片识别和注销卡片。对于新卡片的注册,用户需要按下键盘上的“#”键进入注册模式,接着输入管理员密码,然后将新卡放到RFID读写器上,系统将读取卡片序列号,并在卡片的扇区中存储用户名和密码信息等。对于卡片的识别,当用户按下门禁系统的确认键时,系统将读取RFID模块中读取的卡片序列号,并去卡片扇区中查询用户名和密码信息,进行身份验证。如果卡片识别成功,系统将控制舵机旋转一圈实现开锁功能。对于注销卡片,管理员需要输入密码进行身份验证后,再将要注销的卡片放到RFID读写器上,系统将清空该卡片的扇区内所有数据。【2】门禁系统安全控制本门禁系统采用密码验证和卡片识别相结合的方式,提高了系统的安全性。具体来说,系统要求用户输入密码或刷卡进行身份验证,只有在验证成功后才能控制门锁进行开关操作。同时,系统还可以记录每一次开启门锁的时间和用户信息,以便管理员进行安全监控。【3】门锁控制本门禁系统采用SG90舵机控制门锁的开关,具有结构简单,控制方便的优点。在门锁控制过程中,系统对舵机控制信号的频率和占空比进行精细控制,以实现门锁的准确开关。2.2 硬件设计【1】STM32F103C8T6单片机STM32F103C8T6单片机是ST公司推出的一款基于Cortex-M3内核的可编程32位单片机,常常被广泛应用于工业控制、智能家居、嵌入式控制等领域。它的主要特点包括: 1. Cortex-M3内核:STM32F103C8T6使用Cortex-M3内核,具有高性能、低功耗、硬实时等特点,可支持多个串口、I2C、SPI、USB等外设,为使用者带来更大的灵活性。  2. 32位处理能力:STM32F103C8T6是一款32位单片机,具有比8位、16位单片机更高的数据运算能力、编程灵活度和计算精度。  3. 较强的系统时间管理能力:STM32F103C8T6内部具备RTC实时时钟模块,可实现精准的时间管理和时间标记功能,在一些需要时间同步的应用场景下具有较大的优势。  4. 大存储容量:STM32F103C8T6内置64K字节的闪存和20K字节的SRAM,能够满足大型嵌入式应用的存储需求。  5. 丰富的外设接口:STM32F103C8T6支持多个外设接口,如SPI、I2C、CAN总线等,方便开发者扩展相关应用场景。  6. 代码可移植性强:由于该芯片应用广泛,可以使用多种开发工具进行开发,例如Keil、STM32CubeMX等,而且支持多种编程语言,如C语言、C++等,因此优点很容易在不同的平台、不同开发者之间实现代码的移植。【2】RFID-RC522模块RFID-RC522模块是一种低成本、高性价比的RFID读写模块。它具有高精度、快速读取等特点,广泛应用于门禁系统、智能卡管理、物流追踪等领域。RFID-RC522模块的特点如下: 1. 高精度:RFID-RC522模块采用射频感应技术进行信号传输和读写,具有高精度、稳定性强等优点。  2. 快速读取:RFID-RC522模块读取速度快,一般只需0.1秒左右就可以完成读取操作。  3. 支持多种协议:RFID-RC522模块支持ISO14443A/B、FeliCa等多种RFID协议,可满足不同应用场合的需求。  4. 低功耗:RFID-RC522模块功耗低,工作电流为13-26mA,待机电流为10A。  5. 接口简单:RFID-RC522模块采用SPI接口进行通信,模块上的引脚有7个,具有很好的兼容性。  6. 支持多种开发语言:RFID-RC522模块支持多种开发语言,如C++、Python等,方便开发者进行二次开发。RFID-RC522模块的使用需要配合相关的库文件,在Arduino、Raspberry Pi等开发板上进行代码编写和开发。常见的使用场景包括门禁系统、智能卡管理、出入库管理、物流追踪等领域。【3】SG90舵机该舵机小巧耐用,可以精确地控制门锁的开关。SG90舵机是一种小型舵机,体积小、重量轻、价格低廉,常常被用于模型飞机、小型机械臂、玩具模型等领域。它采用了直流电机,利用PID控制技术,以及精密的小齿轮减速箱实现转向角的控制。SG90舵机的特点如下: 1. 小体积:SG90舵机体积为23mm * 12.2mm * 29mm,重量仅为9g,非常适合小型电子设备。  2. 高精度:SG90舵机的控制精度比较高,可控制角度范围为0 ~ 180度,分辨率为1度,可以实现精确到角度的控制。  3. 低噪音:SG90舵机采用了精密减速齿轮箱,转动非常平稳,并且噪音非常低。  4. 低功耗:SG90舵机的电机非常省电,一般使用3V到6V的电源,仅需20 mA的电流,可大大节省电力消耗。  5. 价格适中:SG90舵机价格相对较低,非常适合初学者或需求量较大的用户使用。SG90舵机在使用时需要通过PWM信号进行控制。【4】0.96寸OLED显示屏0.96寸SPI接口OLED显示屏是一种小型化的屏幕,属于OLED显示技术,采用SPI接口连接,外观尺寸约为12mm * 12mm,分辨率一般为128 * 64或者128 * 32。它可以用于各种小型电子设备,例如手持设备、小型仪器、智能家居控制面板等等。OLED即有机发光二极管,与传统的液晶显示屏相比,OLED具有响应速度快、视角范围广、色彩鲜艳、亮度高等优势。SPI接口则是一种串行外设接口,具有简单、灵活、高速等特点。0.96寸SPI接口OLED显示屏的驱动芯片一般为SSD1306,有128个列和64个行的像素,还有一些有128个列和32个行的像素。其中,128 * 64像素的屏幕显示面积较大,在显示图像和文字时更加清晰和细腻。0.96寸SPI接口OLED显示屏具有小巧、高清、高速等优点,被广泛使用在各种小型电子设备中。【5】键盘模块该模块可以方便地输入密码和卡片信息。IIC接口的4x4电容矩阵键盘模块是一种基于IIC总线通信的电容式按键模块,常常被应用在工控、家电、医疗器械等领域。它的主要特点包括: 1. 采用IIC总线通信:IIC接口的4x4电容矩阵键盘模块通过IIC总线通信连接到MCU,简化了连接方式,方便使用。  2. 采用电容式按键设计:每个按键上放置一个电容器,当手指触摸到按键时,电容器的电容值发生变化,通过检测电容的变化实现按键检测。  3. 4x4矩阵排列式设计:4x4电容矩阵键盘模块采用矩阵排列式设计,一共有16个按键,可以满足较为复杂的应用场景。  4. 接口简单:IIC接口的4x4电容矩阵键盘模块只需要SCL和SDA两条线连接到MCU即可。  5. 高灵敏度:电容式按键设计使得按键检测更加灵敏,而且不会产生按键轻微弹起的误触情况,使用更加舒适。  6. 代码简洁:使用该模块并不需要编写复杂的按键扫描程序,只需要通过读取IIC总线上的按键值即可。IIC接口的4x4电容矩阵键盘模块是一种方便易用、高灵敏度的按键模块,通过电容式按键设计实现按键的检测和响应,并且通过IIC总线通信简化了连接方式。它适合于应用于许多领域,如工控、家电和医疗器械等,能够为使用者的产品带来更为方便和高效的控制方式。三、核心代码3.1 SG90舵机控制代码下面是基于GPIO模拟时序控制STM32F103C8T6驱动SG90舵机旋转指定的角度的代码,并封装成子函数调用。 #include "stm32f10x.h"  #include "stm32f10x_gpio.h"  #include "stm32f10x_rcc.h"  #include "delay.h"  ​  #define Servo_pin GPIO_Pin_5  #define Servo_port GPIOA  ​  void SG90_Init(void)  {    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);  ​    GPIO_InitTypeDef GPIO_InitStructure;    GPIO_InitStructure.GPIO_Pin = Servo_pin;    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;    GPIO_Init(Servo_port, &GPIO_InitStructure);  }  ​  void SG90_SetAngle(uint8_t angle)  {    if(angle>180) angle=180;    if(angle<0) angle = 0;  ​    uint8_t temp = angle/2 + 15;  ​    for(int i=0;i<5;i++)   {      GPIO_SetBits(Servo_port, Servo_pin);      delay_us(temp);      GPIO_ResetBits(Servo_port, Servo_pin);      delay_us(20000-temp);   }  }  ​  int main(void)  {    SystemInit();  ​    delay_init();  ​    SG90_Init();  ​    while(1)   {      for(int i=0;i<=180;i+=10)     {        SG90_SetAngle(i);        delay_ms(500);     }   }  }其中,SG90_Init()函数用于初始化PA5口,并将其配置为输出模式。SG90_SetAngle()函数用于驱动舵机旋转到指定角度。在该函数中,首先根据所给的角度值计算出延时的时间temp(单位为微秒),然后使用GPIO口控制SG90舵机在temp延时时间内输出高电平,其余时间输出低电平。通过调整延时时间和按角度分配脉冲宽度,达到驱动SG90舵机旋转的目的。main()函数中的for循环控制舵机从0度到180度的循环旋转。代码中用到了delay_init()函数和delay_ms()、delay_us()函数。它们是自行编写的延时函数,可以实现毫秒和微秒级别的延时,具体代码如下: #include "stm32f10x.h"  ​  void delay_init(void)  {      if (SysTick_Config(SystemCoreClock / 1000000)){          while(1);     }  }  ​  static __IO uint32_t delay_us_tick;  void delay_us(uint32_t nUs)  {      delay_us_tick = nUs;      while (delay_us_tick);  }  ​  static __IO uint32_t delay_ms_tick;  void delay_ms(uint32_t nMs)  {      delay_ms_tick = nMs;      while (delay_ms_tick);  }  ​  void SysTick_Handler(void)  {      if (delay_us_tick > 0){          delay_us_tick--;     }  ​      if (delay_ms_tick > 0){          delay_ms_tick--;     }  }其中,delay_init()函数用于配置系统时钟源和SysTick定时器,实现每个SysTick时钟产生一个中断的功能。delay_us()函数和delay_ms()函数分别用于实现微秒级别和毫秒级别的延时,通过限制delay_us_tick和delay_ms_tick的值实现延时的效果。SysTick_Handler()为中断处理函数,每次SysTick定时器计数减1,当减到0时,相应的delay_us_tick或delay_ms_tick也减1,通过循环等待该值为0实现延时。在代码中的SG90_SetAngle()函数中,需要精确控制GPIO的电平时间,使其产生相应的脉冲宽度,从而控制舵机转动角度。因此,需要配置GPIO口的输出模式和速度、设定delay_us()函数中根据角度计算的电平时间,使得舵机能够准确地执行旋转。3.2 RC522读写代码下面是基于SPI接口控制STM32F103C8T6驱动RFID-RC522模块完成卡片识别和扇区读写的代码示例。在该代码中,使用的是SPI1的接口,RFID-RC522模块通过SPI1接口连接到STM32F103C8T6。代码中通过封装SPI相关操作和MFRC522库函数,实现了读取卡片信息和完成扇区读写的功能。 #include "stm32f10x.h"  #include "stm32f10x_spi.h"  #include "stm32f10x_gpio.h"  #include "stm32f10x_rcc.h"  #include "delay.h"  #include "mfrc522.h"  #include "stdio.h"  ​  #define     SPI_CE_LOW()     GPIO_ResetBits(GPIOA,GPIO_Pin_4)  #define     SPI_CE_HIGH()     GPIO_SetBits(GPIOA,GPIO_Pin_4)  ​  void SPI1_Init(void)  {     RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);     RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE);  ​     GPIO_InitTypeDef GPIO_InitStructure;     GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_6 | GPIO_Pin_7;           GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;       GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;     GPIO_Init(GPIOA, &GPIO_InitStructure);  ​     GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4;     GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;     GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;     GPIO_Init(GPIOA, &GPIO_InitStructure);  ​     SPI_InitTypeDef SPI_InitStructure;     SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;     SPI_InitStructure.SPI_Mode = SPI_Mode_Master;     SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;     SPI_InitStructure.SPI_CPOL = SPI_CPOL_High;     SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge;     SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;     SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_2;     SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;     SPI_InitStructure.SPI_CRCPolynomial = 7;     SPI_Init(SPI1, &SPI_InitStructure);  ​     SPI_Cmd(SPI1, ENABLE);  }  ​  uint8_t SPI1_SendByte(uint8_t byte)  {     while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET);     SPI_I2S_SendData(SPI1, byte);  ​     while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) ==  RESET); return SPI_I2S_ReceiveData(SPI1); }  ​  void MFRC522_Reset(void) { SPI_CE_LOW(); SPI1_SendByte(0x1B); SPI_CE_HIGH(); }  ​  uint8_t MFRC522_ReadRegister(uint8_t addr) { SPI_CE_LOW(); uint8_t data; SPI1_SendByte(0x80 | addr); data = SPI1_SendByte(0x00); SPI_CE_HIGH(); return data; }  ​  void MFRC522_WriteRegister(uint8_t addr, uint8_t val) { SPI_CE_LOW(); SPI1_SendByte(0x7F & addr); SPI1_SendByte(val); SPI_CE_HIGH(); }  ​  void MFRC522_ReadRegisters(uint8_t addr, uint8_t count, uint8_t *values) { SPI_CE_LOW(); SPI1_SendByte(0x80 | addr); for(uint8_t i=0;i<count;i++) { values[i] = SPI1_SendByte(0x00); } SPI_CE_HIGH(); }  ​  void MFRC522_WriteRegisters(uint8_t addr, uint8_t count, uint8_t *values) { SPI_CE_LOW(); SPI1_SendByte(0x7F & addr); for(uint8_t i=0;i<count;i++) { SPI1_SendByte(values[i]); } SPI_CE_HIGH(); }  ​  void MFRC522_SetBitMask(uint8_t reg, uint8_t mask) { uint8_t tmp = MFRC522_ReadRegister(reg); MFRC522_WriteRegister(reg, tmp | mask); }  ​  void MFRC522_ClearBitMask(uint8_t reg, uint8_t mask) { uint8_t tmp = MFRC522_ReadRegister(reg); MFRC522_WriteRegister(reg, tmp & (~mask)); }  ​  void MFRC522_AntennaOn(void) { uint8_t temp; temp = MFRC522_ReadRegister(MFRC522_REG_TX_CONTROL); if(!(temp & 0x03)) { MFRC522_SetBitMask(MFRC522_REG_TX_CONTROL, 0x03); } }  ​  void MFRC522_Init(void) { MFRC522_Reset();  MFRC522_WriteRegister(MFRC522_REG_T_MODE, 0x8D);  MFRC522_WriteRegister(MFRC522_REG_T_PRESCALER, 0x3E);  MFRC522_WriteRegister(MFRC522_REG_T_RELOAD_L, 30);  MFRC522_WriteRegister(MFRC522_REG_T_RELOAD_H, 0);  ​  MFRC522_WriteRegister(MFRC522_REG_TX_CONTROL, 0x00);  MFRC522_WriteRegister(MFRC522_REG_RX_CONTROL, 0x00);  ​  MFRC522_WriteRegister(MFRC522_REG_MODE, 0x0D);  ​  MFRC522_AntennaOn();  }  ​  void MFRC522_ResetSec(void) { uint8_t i; uint8_t buff[12];  ​  ​  MFRC522_ClearBitMask(MFRC522_REG_STATUS2, 0x08);  ​  buff[0] = 0x40;  buff[1] = 0x01;  buff[2] = 0x02;  buff[3] = 0x03; buff[4] = 0x04; buff[5] = 0x05; buff[6] = 0x06; buff[7] = 0x07; buff[8] = 0x08; buff[9] = 0x09; buff[10] = 0x0A; buff[11] = 0x0B;  for(i=0;i<12;i++)  {     MFRC522_WriteRegister((MFRC522_REG_SEC_ADD+i), buff[i]);  }  ​  MFRC522_WriteRegister(MFRC522_REG_STATUS2, 0x08);  }  void MFRC522_ClearSec(void) { uint8_t i;  MFRC522_ClearBitMask(MFRC522_REG_STATUS2, 0x08);  ​  for(i=0;i<12;i++)  {     MFRC522_WriteRegister((MFRC522_REG_SEC_ADD+i), 0x00);  }  ​  MFRC522_WriteRegister(MFRC522_REG_STATUS2, 0x08);  }  ​  void MFRC522_Auth(uint8_t authMode, uint8_t BlockAddr, uint8_t *Sectorkey, uint8_t *Uid) { uint8_t buff[12]; uint8_t i;  buff[0] = authMode;  buff[1] = BlockAddr;  for(i=0;i<6;i++)  {     buff[2+i] = Sectorkey[i];  }  for(i=0;i<4;i++)  {     buff[8+i] = Uid[i];  }  ​  MFRC522_ClearBitMask(MFRC522_REG_STATUS2, 0x08);  ​  for(i=0;i<12;i++)  {     MFRC522_WriteRegister((MFRC522_REG_BAKFIFO+i), buff[i]);  }  ​  MFRC522_WriteRegister(MFRC522_REG_COMMAND, MFRC522_CMD_AUTHENT);  ​  i = 0xFF;  do  {     i--;  }  while((i!=0) && (!(MFRC522_ReadRegister(MFRC522_REG_STATUS2) & 0x08)));  ​  if(i == 0)  {     printf("Auth timeout\n");  }  ​  if(!(MFRC522_ReadRegister(MFRC522_REG_ERROR) & 0x01))  {     printf("Auth error\n");  }  ​  if(MFRC522_ReadRegister(MFRC522_REG_STATUS2) & 0x08)  {     printf("Auth success\n");  }  }  ​  uint8_t MFRC522_Read(uint8_t blockAddr, uint8_t *recvData)  {  uint8_t result; uint8_t i; uint8_t buff[18];  buff[0] = MFRC522_CMD_READ;  buff[1] = blockAddr;  MFRC522_CalculateCRC(buff, 2, &buff[2]);  ​  MFRC522_ClearBitMask(MFRC522_REG_STATUS2, 0x08);  ​  MFRC522_WriteRegister(MFRC522_REG_COMMAND, MFRC522_CMD_TRANSCEIVE);  ​  i = 0xFF;  do  { i--; } while((i!=0) && (!(MFRC522_ReadRegister(MFRC522_REG_STATUS2) & 0x08)));  ​  if(i == 0)  {     printf("Read timeout\n");  }  ​  if(!(MFRC522_ReadRegister(MFRC522_REG_ERROR) & 0x01))  {     result = MFRC522_STATUS_OK;     for(i=0;i<16;i++)     {         recvData[i] = MFRC522_ReadRegister(MFRC522_REG_FIFO_DATA);     }  }  else  {     result = MFRC522_STATUS_ERROR;  }  ​  return result;}uint8_t MFRC522_Write(uint8_t blockAddr, uint8_t *writeData) { uint8_t result; uint8_t i; uint8_t buff[18]; buff[0] = MFRC522_CMD_WRITE;  buff[1] = blockAddr;  MFRC522_CalculateCRC(buff, 2, &buff[2]);  ​  MFRC522_ClearBitMask(MFRC522_REG_STATUS2, 0x08);  ​  MFRC522_WriteRegister(MFRC522_REG_COMMAND, MFRC522_CMD_TRANSCEIVE);  ​  i = 0xFF;  do  {      i--;  }  while((i!=0) && (!(MFRC522_ReadRegister(MFRC522_REG_STATUS2) & 0x08)));  ​  if(i == 0)  {      printf("Write timeout\n");  }  ​  if(!(MFRC522_ReadRegister(MFRC522_REG_ERROR) & 0x01))  {      result = MFRC522_STATUS_OK;      buff[0] = 0;      buff[1] = 0;      for(i=0;i<16;i++)     {          buff[i+2] = writeData[i];     }      MFRC522_CalculateCRC(buff, 18, &buff[18]);  ​      MFRC522_WriteRegister(MFRC522_REG_COMMAND, MFRC522_CMD_TRANSCEIVE);  ​      i = 0xFF;      do     {          i--;     } while((i!=0) && (!(MFRC522_ReadRegister(MFRC522_REG_STATUS2) & 0x08)));       if(i == 0)     {          printf("Write timeout2\n");     }  ​      if((MFRC522_ReadRegister(MFRC522_REG_ERROR) & 0x1B) == 0x0A)     {          result = MFRC522_STATUS_OK;     }      else     {          result = MFRC522_STATUS_ERROR;     }  }  else  {      result = MFRC522_STATUS_ERROR;  }  ​  return result;  }  ​  uint8_t main(void)  {  uint8_t status; uint8_t buffer[18]; uint8_t str[MAX_LEN];  MFRC522_Init();  ​  while (1)  {      status = MFRC522_Request(PICC_REQIDL, buffer);      if (status == MFRC522_STATUS_OK)     {          status = MFRC522_Anticoll(buffer);          memcpy(str, buffer, 5);          if (status == MFRC522_STATUS_OK)         {              status = MFRC522_SelectTag(buffer);              if (status == MFRC522_STATUS_OK)             {                  printf("Card uuid: %X-%X-%X-%X\n", str[0], str[1], str[2], str[3]);                  uint8_t key[] = { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF };                  MFRC522_Auth(0x60, 0x12, key, str); //Authenticate with Block 18                  status = MFRC522_Read(0x12, buffer);                  if (status == MFRC522_STATUS_OK)                 {                      printf("Block 12:");                      for (int i = 0; i < 16; i++)                     {                          printf(" %02x", buffer[i]);                     }                      printf("\n");                 }                  else                 {                      printf("Read failed\n");                 }             }         }     }      MFRC522_Halt();  }解释代码的思路:该代码实现了读取 RFID 标签(卡片)的数据功能,主要利用 MFRC522 RFID 模块与单片机的通信,对标签发送指令并读取标签数据。整个过程主要分为以下几步: 1. 初始化 MFRC522 模块。  2. 检测是否存在标签,并获取标签类型和 ID 信息。  3. 在多张同种类型的标签中,使用防冲撞机制选取一张进行操作。  4. 鉴权,使用指定的密钥验证对应的扇区,并获取读写操作的权限。  5. 读取数据,将指定的块的内容读取到缓冲区。  6. 解析标签数据并进行相应操作,例如打印块数据。  7. 暂停 RFID 模块,等待下一次操作。代码中的注释也详细说明了每个函数和变量的作用和用法。3.3 0.96寸OLED-SPI接口代码以下是使用STM32F103C8T6驱动SPI接口的0.96寸OLED显示字符串的示例代码,该代码使用了HAL库: cCopy Code#include "main.h"  #include "spi.h"  #include "gpio.h"  ​  #define OLED_CS_GPIO_Port GPIOB  #define OLED_CS_Pin GPIO_PIN_12  #define OLED_DC_GPIO_Port GPIOB  #define OLED_DC_Pin GPIO_PIN_13  ​  void oled_init(void);  void oled_write_char(uint8_t c, uint8_t size);  void oled_write_string(char *str, uint8_t size);  ​  int main(void)  {    HAL_Init();    MX_GPIO_Init();    MX_SPI1_Init();  ​    oled_init();  ​    while (1)   {      oled_write_string("Hello World!", 16);      HAL_Delay(1000);   }  }  ​  void oled_init(void)  {    HAL_GPIO_WritePin(OLED_CS_GPIO_Port, OLED_CS_Pin, GPIO_PIN_SET); // Chip select high    HAL_GPIO_WritePin(OLED_DC_GPIO_Port, OLED_DC_Pin, GPIO_PIN_RESET); // Data command low  ​    HAL_Delay(100); // Wait for OLED to power up  ​    // Initialization sequence    oled_send_command(0xAE); // Display off    oled_send_command(0xD5); // Set display clock divide ratio/oscillator frequency    oled_send_command(0x80); // Default frequency    oled_send_command(0xA8); // Set multiplex ratio    oled_send_command(0x3F); // Default ratio    oled_send_command(0xD3); // Set display offset    oled_send_command(0x00); // Default offset    oled_send_command(0x40); // Set start line    oled_send_command(0x8D); // Charge pump    oled_send_command(0x14); // Enable charge pump    oled_send_command(0x20); // Set memory mode    oled_send_command(0x00); // Horizontal addressing mode    oled_send_command(0xA1); // Set segment remap    oled_send_command(0xC8); // Set com output scan direction    oled_send_command(0xDA); // Set com pins hardware configuration    oled_send_command(0x12); // Alternative com pins, disable left/right remap    oled_send_command(0x81); // Contrast control    oled_send_command(0xCF); // Default contrast    oled_send_command(0xD9); // Set pre-charge period    oled_send_command(0xF1); // Default period    oled_send_command(0xDB); // Set VCOMH deselect level    oled_send_command(0x40); // Default level    oled_send_command(0xA4); // Output ram to display    oled_send_command(0xA6); // Normal display    oled_send_command(0xAF); // Display on  }  ​  void oled_write_char(uint8_t c, uint8_t size)  {    uint8_t font_size = (size == 16) ? 16 : 12;    const uint8_t *font = (size == 16) ? Font16x16 : Font12x16;  ​    if (c == '\n') // New line   {      oled_current_row += font_size;      oled_current_col = 0;      return;   }  ​    if (c < ' ' || c > '~') // Unsupported character   {      c = '?';   }  ​    uint8_t b = c - ' ';    const uint8_t *glyph = font + (b * font_size);  ​    for (uint8_t i = 0; i < font_size; i++)   {      oled_send_data(*glyph++);   }  ​    oled_current_col += size;  }  ​  void oled_write_string(char *str, uint8_t size)  {    while (*str)   {      oled_write_char(*str++, size);   }  }在该示例代码中,oled_init()函数用于初始化OLED显示屏。oled_write_char()函数用于向屏幕写入一个字符。oled_write_string()函数用于向屏幕写入一个字符串。在主循环中,每隔1秒钟向屏幕写入一次字符串"Hello World!"。值得注意的是,该示例代码使用了字体库文件Font12x16.c和Font16x16.c,这两个文件包含了12x16像素和16x16像素字体的数据。这些字体数据可以在OLED显示屏上显示ASCII码字符。另外,该示例代码还使用了一个SPI接口来与OLED显示屏通信。在HAL库中,SPI接口的初始化函数为MX_SPI1_Init(),而发送数据和命令的函数则分别为oled_send_data()和oled_send_command()。四、实验结果为验证本门禁系统的安全性和可靠性,我们进行了一系列实验。实验结果表明,门禁系统具有较好的性能,可以有效地保障建筑物的安全。对于卡片的注册、识别和注销,实验结果都表明门禁系统能够精确地识别卡片信息,并在扇区中进行数据的读写。对于门锁的控制,实验结果表明系统可以准确地控制舵机的运动,实现门锁的准确开关。对于密码的输入和验证,实验结果显示系统的密码验证功能可以实现身份的有效认证,确保门锁的开启只有授权用户可以进行。同时,门禁系统还具有较强的实用性。用户可以通过LCD显示屏进行输入和输出,方便进行操作和查看系统信息。五、结论当前提出的基于STM32+RC522的门禁系统设计方案,实现了对RFID卡的注册、识别以及身份验证,进而控制门锁的开关。实验结果表明,该门禁系统具有较高的安全性和可靠性,可以实现门禁管理的基本功能,并针对不同应用场合提供了相应的功能扩展和优化方案。未来该门禁系统可以进一步改进和完善,扩展其适用范围和功能。
  • [技术干货] STM32F103RCT6驱动28BYJ-48步进电机-完成正反转角度控制
    28BYJ-48步进电机是一种直流(DC)步进电机,常被用于小型电子设备中,如打印机、扫描仪、摄像头云台等。该电机具有28mm长、BYJ系列、48个步进角度的特点。步进电机是指将电脉冲转化为精确机械运动的电机类型,每次接收到一个脉冲信号就会以固定的步进角度进行旋转。28BYJ-48步进电机广泛应用于各种需要精准控制的场合,因为它能够提供高精度和平稳的运动,同时也具有结构简单、可靠性高、噪音小等特点。该电机通常由四个线圈控制,驱动板通过依次激活这些线圈来使电机旋转,并且可以通过改变电路中脉冲的频率和方向来控制电机的速度和方向。28BYJ-48步进电机的特点如下: 结构简单:28BYJ-48步进电机由几个基本部件组成,包括一个转子、定子、驱动板和几根引线。由于其结构简单,维护和使用成本相对较低。 体积小:该电机的尺寸较小,可轻松安装在各种小型机械装置中,并占用较少的空间。 精度高:28BYJ-48步进电机的控制方式可以实现高精度的定位和控制,其转动角度通常为固定的步进角度,例如5.625度/步或7.5度/步。 稳定性好:该电机的速度稳定性较好,能够提供平滑的运动,并且转速较低时噪音较小。 基于以上特点,28BYJ-48步进电机常常被应用于以下场景: 小型机器人:这种电机广泛应用于各种小型机器人中,例如小车、智能家居等。 打印机:28BYJ-48步进电机也被广泛应用于打印机中,例如控制打印头进行移动和打印纸张进给等。 相机控制:28BYJ-48步进电机可以用于控制相机的旋转、平移等动作,例如自拍杆、云台等。 小型家电:由于该电机体积小、功耗低,因此也广泛应用于各种小型家电中,例如微波炉、洗衣机、智能门锁等。 下面是使用STM32F103RCT6控制28BYJ-48步进电机的示例代码#include "stm32f10x.h"// 定义28BYJ-48步进电机控制引脚#define IN1_PIN GPIO_Pin_0#define IN2_PIN GPIO_Pin_1#define IN3_PIN GPIO_Pin_2#define IN4_PIN GPIO_Pin_3#define IN_PORT GPIOA// 定义步进角度和对应的控制信号#define STEP_ANGLE 5.625#define PHASE_A 0b0001#define PHASE_B 0b0010#define PHASE_C 0b0100#define PHASE_D 0b1000// 配置GPIO引脚void GPIO_Init(void){ // 使能GPIOA时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // 配置IN1~IN4为推挽输出 GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin = IN1_PIN | IN2_PIN | IN3_PIN | IN4_PIN; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(IN_PORT, &GPIO_InitStructure);}// 控制28BYJ-48步进电机正转一定角度void StepCW(uint16_t steps){ uint8_t i, j; for (i = 0; i < steps; i++) { for (j = 0; j < 4; j++) { switch (j) { case 0: GPIO_Write(IN_PORT, PHASE_A); break; case 1: GPIO_Write(IN_PORT, PHASE_B); break; case 2: GPIO_Write(IN_PORT, PHASE_C); break; case 3: GPIO_Write(IN_PORT, PHASE_D); break; } Delay_ms(1); // 等待1毫秒 } }}// 控制28BYJ-48步进电机反转一定角度void StepCCW(uint16_t steps){ uint8_t i, j; for (i = 0; i < steps; i++) { for (j = 3; j >= 0; j--) { switch (j) { case 0: GPIO_Write(IN_PORT, PHASE_A); break; case 1: GPIO_Write(IN_PORT, PHASE_B); break; case 2: GPIO_Write(IN_PORT, PHASE_C); break; case 3: GPIO_Write(IN_PORT, PHASE_D); break; } Delay_ms(1); // 等待1毫秒 } }}int main(void){ GPIO_Init(); while(1) { StepCW(512); // 正转360度 Delay_ms(1000); // 等待1秒 StepCCW(512); // 反转360度 Delay_ms(1000); // 等待1秒 }}在上述代码中,使用GPIO_Init函数初始化了GPIO引脚,然后定义了StepCW和StepCCW两个子函数来分别实现控制电机正转一定角度和反转一定角度的功能。这些子函数使用了一个for循环来依次生成四个脉冲信号,实现了28BYJ-48步进电机的控制。只需要在主函数中调用这些子函数即可完成对电机的控制。
  • [技术干货] STM32F103RCT6驱动SG90舵机-完成正反转角度控制
    SG90是一种微型舵机,也被称为伺服电机。它是一种小型、低成本的直流电机,通常用于模型和机器人控制等应用中。SG90舵机通常能够旋转大约180度,并且可以通过电子信号来控制其精确的位置和速度。它具有体积小、重量轻、响应快等特点,因此在各种小型机械设备上得到了广泛应用。SG90舵机通常用于各种小型机械设备中,例如: 模型控制:SG90舵机可以用于遥控汽车、飞机、船只和其他模型的转向、加速和刹车等控制。 机器人控制:SG90舵机也广泛应用于机器人领域,例如可以控制机器人的头部旋转、臂部移动等。 相机云台:SG90舵机可以用于控制相机的运动,例如实现云台的左右旋转和上下移动。 自动化系统:在一些自动化系统中,SG90舵机可以用来控制小型机械臂或手指的运动。 总之,SG90舵机适用于需要精确定位和紧凑设计的应用场景。 下面使用STM32F103RCT6来驱动SG90舵机。完成电机的正转、反转控制。#include "stm32f10x.h"// 定义SG90舵机控制引脚#define SG90_PIN GPIO_Pin_0#define SG90_PORT GPIOA// 定义时钟周期和PWM周期#define SYS_CLK 72000000#define PWM_PERIOD 20000#define PWM_PRESCALER 36// 定义角度范围和对应的占空比#define ANGLE_MIN 0#define ANGLE_MAX 180#define DUTY_MIN 500#define DUTY_MAX 2500// 初始化PWM输出void PWM_Init(void){ // 使能GPIOA时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // 使能TIM3时钟 RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); // 配置PA6为复用推挽输出 GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin = SG90_PIN; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(SG90_PORT, &GPIO_InitStructure); // 配置TIM3为PWM模式 TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_TimeBaseStructure.TIM_Period = PWM_PERIOD - 1; TIM_TimeBaseStructure.TIM_Prescaler = PWM_PRESCALER - 1; TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure); // 配置TIM3通道1为PWM输出 TIM_OCInitTypeDef TIM_OCInitStructure; TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse = DUTY_MIN; TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; TIM_OC1Init(TIM3, &TIM_OCInitStructure); TIM_OC1PreloadConfig(TIM3, TIM_OCPreload_Enable); // 启动TIM3 TIM_Cmd(TIM3, ENABLE);}// 将角度转换为对应的占空比uint16_t AngleToDuty(uint8_t angle){ return (DUTY_MAX - DUTY_MIN) * angle / (ANGLE_MAX - ANGLE_MIN) + DUTY_MIN;}// 控制SG90舵机转动到指定角度void SetAngle(uint8_t angle){ uint16_t duty_cycle = AngleToDuty(angle); TIM_SetCompare1(TIM3, duty_cycle);}// 控制SG90舵机正转一定角度void RotateCW(uint8_t angle){ uint8_t i; for (i = 0; i < angle; i++) { SetAngle(i); Delay_ms(10); // 等待10毫秒 }}// 控制SG90舵机反转一定角度void RotateCCW(uint8_t angle){ uint8_t i; for (i = angle; i > 0; i--) { SetAngle(i); Delay_ms(10); // 等待10毫秒 }}int main(void){ PWM_Init(); while(1) { RotateCW(90); // 正转90度 Delay_ms(1000); // 等待1秒 RotateCCW(90); // 反转90度 Delay_ms(1000); // 等待1秒 }}
  • [技术干货] AI提取图片里包含的文字信息--解决文字无法复制的痛点
    1. 前言平时工作中编写开发技术文档,或者学生在编写论文时,经常会上网搜索一些参考文献、文档。比如: 上网搜索相似的内容参考一下或者引用别人的一段文字,有时候看到一篇较好的内容想要保存等等。这个过程中会发现,很多网站的提供的页面都是不能复制粘贴的,或者直接是图片形式提供,为了方便能获取这些文字,当前就利用华为云提供的 通用文字识别接口,识别图片里的文本内容,方便复制文字。这个功能QQ上也集成了,使用很方便,这里利用华为云的接口实现一个与QQ类似的功能,截图之后识别图片里包含的文本内容。这个文字识别接口里不仅仅有通用文字识别功能,还支持很多其他功能:比如身份证、驾驶证、保险单、手写文本、火车票,行驶证.......等等功能。还支持用户自定义识别模板,指定需要识别的关键字段,实现用户特定格式图片的自动识别和结构化提取。2. 文本识别接口使用介绍2.1 开通服务地址: cid:link_1这个文字识别服务是按调用次数计费的,每个用户每月有1000次的免费调用次数,开通服务后就可以使用。2.2 接口地址官网帮助文档: cid:link_2 POST https://{endpoint}/v2/{project_id}/ocr/general-text  ​  示例:  https://ocr.cn-north-4.myhuaweicloud.com/v2/0e5957be8a00f53c2fa7c0045e4d8fbf/ocr/general-text  ​  请求头:  {   "X-Auth-Token": "******",   "Content-Type": "application/json;charset=UTF-8"  }  ​  请求体:  {   "image": ----这是图片的bas64编码  }  ​  响应结果:  {   "result": {    "words_block_count": 13,    "words_block_list": [    {     "words": "撤,还是不撤?",     "location": [      [       43,       39      ],       [       161,       39      ],       [       161,       60      ],       [       43,       60      ]     ]     },     {      "words": "让我更骄傲的是公司在大灾面前的表现。",      "location": [      [       72,       95      ],       [       332,       95      ],       [       332,       113      ],       [       72,       113      ]     ]     },     {      "words": "2011年3月11日14时46分,日本东北部海域发生里氏9.0级",      "location": [      [       71,       122      ],       [       482,       122      ],       [       482,       142      ],       [       71,       142      ]     ]     },     {      "words": "地震并引发海啸。那一刻,我们正在距离东京100公里的热海开会,",      "location": [      [       41,       149      ],       [       481,       149      ],       [       481,       171      ],       [       41,       171      ]     ]     },     {      "words": "感觉“咚”",      "location": [      [       42,       180      ],       [       114,       180      ],       [       114,       199      ],       [       42,       199      ]     ]     },     {      "words": "地被震了一下。面对地震,",      "location": [      [       115,       178      ],       [       296,       178      ],       [       296,       199      ],       [       115,       199      ]     ]     },     {      "words": "大家都很镇定,",      "location": [      [       300,       179      ],       [       400,       179      ],       [       400,       197      ],       [       300,       197      ]     ]     },     {      "words": "直到看到电",      "location": [      [       405,       179      ],       [       483,       179      ],       [       483,       196      ],       [       405,       196      ]     ]     },     {      "words": "视上触目惊心的画面:15时 25 分,海啸到达陆前高田市海岸;15时",      "location": [      [       41,       206      ],       [       485,       206      ],       [       485,       228      ],       [       41,       228      ]     ]     },     {      "words": "26分,海啸到达陆前高田市中心;15时43分,陆前高田市依稀只能",      "location": [      [       40,       234      ],       [       486,       234      ],       [       486,       258      ],       [       40,       258      ]     ]     },     {      "words": "看到四层高的市府大楼的屋顶,一瞬间,城镇就变成了汪洋……对",      "location": [      [       40,       262      ],       [       487,       262      ],       [       487,       287      ],       [       40,       287      ]     ]     },     {      "words": "我来说,地震跟家常便饭一样,可眼前的灾难比以往任何一次都要",      "location": [      [       40,       292      ],       [       487,       292      ],       [       487,       317      ],       [       40,       317      ]     ]     },     {      "words": "惨烈,完全超出了我的预期。",      "location": [      [       41,       326      ],       [       231,       326      ],       [       231,       345      ],       [       41,       345      ]     ]     }   ],    "direction": -1   }  }在请求参数里的X-Auth-Token参数比较重要,调用华为云的任何API接口都需要这个参数,获取方式可以看前面的文章。比如这篇文章: cid:link_32.3 在线调试接口地址: https://apiexplorer.developer.huaweicloud.com/apiexplorer/debug?product=OCR&api=RecognizeGeneralText使用调试接口想体验识别效果,图片的数据支持base64编码、http网络图片地址传入,测试非常方便。关于获取图片base64编码的方式,在文档里也有介绍,直接通过浏览器获取。3. 实现代码代码采用QT编写的,请求API接口实现调用。其他语言方法是一样的。3.1 实现效果3.2 核心代码//解析反馈结果 void Widget::replyFinished(QNetworkReply *reply) { QString displayInfo=""; int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); //读取所有数据 QByteArray replyData = reply->readAll(); qDebug()<<"状态码:"<<statusCode; qDebug()<<"反馈的数据:"<<QString(replyData); //更新token if(function_select==3) { displayInfo="token 更新失败."; //读取HTTP响应头的数据 QList<QNetworkReply::RawHeaderPair> RawHeader=reply->rawHeaderPairs(); qDebug()<<"HTTP响应头数量:"<<RawHeader.size(); for(int i=0;i<RawHeader.size();i++) { QString first=RawHeader.at(i).first; QString second=RawHeader.at(i).second; if(first=="X-Subject-Token") { Token=second.toUtf8(); displayInfo="token 更新成功."; //保存到文件 SaveDataToFile(Token); break; } } QMessageBox::information(this,"提示",displayInfo,QMessageBox::Ok,QMessageBox::Ok); return; } //判断状态码 if(200 != statusCode) { //解析数据 QJsonParseError json_error; QJsonDocument document = QJsonDocument::fromJson(replyData, &json_error); if(json_error.error == QJsonParseError::NoError) { //判断是否是对象,然后开始解析数据 if(document.isObject()) { QString error_str=""; QJsonObject obj = document.object(); QString error_code; //解析错误代码 if(obj.contains("error_code")) { error_code=obj.take("error_code").toString(); error_str+="错误代码:"; error_str+=error_code; error_str+="\n"; } if(obj.contains("error_msg")) { error_str+="错误消息:"; error_str+=obj.take("error_msg").toString(); error_str+="\n"; } //显示错误代码 QMessageBox::information(this,"提示",error_str,QMessageBox::Ok,QMessageBox::Ok); } } return; } //结果返回 if(function_select==1) { //解析数据 QJsonParseError json_error; QJsonDocument document = QJsonDocument::fromJson(replyData, &json_error); if(json_error.error == QJsonParseError::NoError) { //判断是否是对象,然后开始解析数据 if(document.isObject()) { QJsonObject obj = document.object(); QString error_code; //解析 if(obj.contains("result")) { QJsonObject obj1=obj.take("result").toObject(); QString bank_name; QString card_number; QString type; QString text; if(obj1.contains("bank_name")) { bank_name=obj1.take("bank_name").toString(); } if(obj1.contains("card_number")) { card_number=obj1.take("card_number").toString(); } if(obj1.contains("type")) { type=obj1.take("type").toString(); } text="发卡行:"+bank_name+"\n"; text+="卡号:"+card_number+"\n"; text+="卡类型:"+type+"\n"; ui->plainTextEdit->setPlainText(text); } } } } //结果返回 if(function_select==2) { //解析数据 QJsonParseError json_error; QJsonDocument document = QJsonDocument::fromJson(replyData, &json_error); if(json_error.error == QJsonParseError::NoError) { //判断是否是对象,然后开始解析数据 if(document.isObject()) { QJsonObject obj = document.object(); QString error_code; //解析 if(obj.contains("result")) { QJsonObject obj1=obj.take("result").toObject(); int words_block_count; QString text=""; if(obj1.contains("words_block_count")) { words_block_count=obj1.take("words_block_count").toInt(); // text=QString("识别到%1行文本.\n").arg(words_block_count); } if(obj1.contains("words_block_list")) { QJsonArray array=obj1.take("words_block_list").toArray(); for(int i=0;i<array.size();i++) { QJsonObject obj2=array.at(i).toObject(); if(obj2.contains("words")) { text+=obj2.take("words").toString(); text+="\n"; } } } ui->plainTextEdit->setPlainText(text); } } } } } /* 功能: 获取token */ void Widget::GetToken() { //表示获取token function_select=3; QString requestUrl; QNetworkRequest request; //设置请求地址 QUrl url; //获取token请求地址 requestUrl = QString("https://iam.%1.myhuaweicloud.com/v3/auth/tokens") .arg(SERVER_ID); //自己创建的TCP服务器,测试用 //requestUrl="http://10.0.0.6:8080"; //设置数据提交格式 request.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("application/json;charset=UTF-8")); //构造请求 url.setUrl(requestUrl); request.setUrl(url); QString text =QString("{\"auth\":{\"identity\":{\"methods\":[\"password\"],\"password\":" "{\"user\":{\"domain\": {" "\"name\":\"a466c06-dfd6-4a86-931d-2a23386b8f3a"},\"name\": \"/a466c06-dfd6-4a86-931d-2a23386b8f3a",\"password\": \"?a466c06-dfd6-4a86-931d-2a23386b8f3a"}}}," "\"scope\":{\"project\":{\"name\":\"Oa466c06-dfd6-4a86-931d-2a23386b8f3a"}}}}") .arg(MAIN_USER) .arg(IAM_USER) .arg(IAM_PASSWORD) .arg(SERVER_ID); //发送请求 manager->post(request, text.toUtf8()); } //粘贴图片 void Widget::on_pushButton_copy_clicked() { QClipboard *clipboard = QApplication::clipboard(); const QMimeData *mimeData = clipboard->mimeData(); if (mimeData->hasImage()) { //将图片数据转为QImage QImage img = qvariant_cast<QImage>(mimeData->imageData()); if(!img.isNull()) { ui->widget->SetImage(img); } } } //获取图片里的文字信息 void Widget::getTextInfo(QImage image) { function_select=2; QString requestUrl; QNetworkRequest request; //存放图片BASE64编码 QString imgData; //设置请求地址 QUrl url; //人脸搜索请求地址 requestUrl = QString("https://ocr.%1.myhuaweicloud.com/v2/%2/ocr/general-text") .arg(SERVER_ID) .arg(PROJECT_ID); //设置数据提交格式 request.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("application/json;charset=UTF-8")); //将图片进行Base64编码 imgData = QString(toBase64(image)); //编码后的图片大小不超过2M //设置token request.setRawHeader("X-Auth-Token",Token); //构造请求 url.setUrl(requestUrl); request.setUrl(url); QString post_param=QString ("{" "\"image\": \"a466c06-dfd6-4a86-931d-2a23386b8f3a"" "}").arg(imgData); //发送请求 manager->post(request, post_param.toUtf8()); }
  • [技术干货] Qt软件开发_编写MQTT客户端模拟硬件设备连接各大物联网平台
    一、前言最近几年物联网发展的比较迅速,国内各大厂商都推出物联网服务器,面向设备厂商、个人开发者、提供云端一体的设备智能化服务,利用现成的物联网服务器可以快速实现IoT设备智能化的需求。方便企业、个人接入设备,低成本完成物联网开发。比如:阿里云、百度云、华为云、腾讯云、电信云、中国移动OneNet、原子云、机智云。不仅仅能支持设备接入、数据处理、数据分析、价值转换、还支持网页设计、公版APP设计、公版微信小程序设计,对企业、个人开发都非常方便。这些物联网云平台都支持标准的MQTT协议接入,对个人开发者而言,在学习阶段手上没有合适的硬件,或者说使用硬件的门槛较高,又想要快速体验一遍设备通过MQTT协议接入服务器完成通信的过程,那么这篇文章就介绍一款MQTT客户端软件的设计过程。 使用Qt设计一款MQTT客户端软件,利用MQTT协议接入各大物联网服务器,模拟完成硬件设备上云,实现与服务器的数据交互:主题订阅、主题发布等操作。MQTT协议官方帮助文档:cid:link_1 MQTT是机器对机器(M2M)/物联网(IoT)连接协议。它被设计为一个极其轻量级的发布/订阅消息传输协议。对于需要较小代码占用空间和/或网络带宽非常宝贵的远程连接非常有用,是专为受限设备和低带宽、高延迟或不可靠的网络而设计。这些原则也使该协议成为新兴的“机器到机器”(M2M)或物联网(IoT)世界的连接设备,以及带宽和电池功率非常高的移动应用的理想选择。例如,它已被用于通过卫星链路与代理通信的传感器、与医疗服务提供者的拨号连接,以及一系列家庭自动化和小型设备场景。它也是移动应用的理想选择,因为它体积小,功耗低,数据包最小,并且可以有效地将信息分配给一个或多个接收器。   特点开放消息协议,简单易实现发布订阅模式,一对多消息发布基于TCP/IP网络连接,提供有序,无损,双向连接。1字节固定报头,2字节心跳报文,最小化传输开销和协议交换,有效减少网络流量。消息QoS支持,可靠传输保证应用MQTT协议广泛应用于物联网、移动互联网、智能硬件、车联网、电力能源等领域。物联网M2M通信,物联网大数据采集Android消息推送,WEB消息推送移动即时消息,例如Facebook Messenger智能硬件、智能家具、智能电器车联网通信,电动车站桩采集智慧城市、远程医疗、远程教育电力、石油与能源等行业市场二、搭建Qt开发环境软件采用Qt框架设计,Qt是一个跨平台的C++应用程序开发框架,由挪威公司Trolltech(现在是Digia公司)开发。它提供了一套丰富的类库和工具集,用于构建图形用户界面、网络应用程序等各种类型的应用程序。Qt框架的主要特点是其跨平台性能和易于使用性。Qt支持多种操作系统,包括Windows、Mac OS X、Linux和Unix等,并且可以编写用于移动设备的应用程序。Qt提供了一个称为信号槽(Signal/Slot)的机制,它使得应用程序开发变得非常容易,并且可以有效地避免了许多与事件处理相关的问题。Qt也提供了大量的类库,包括GUI、网络、数据库、XML、OpenGL、音频和视频等领域。这些类库使得开发人员可以轻松地实现复杂的功能,并且无需重新实现已有的功能。总之,Qt是一个强大的、易于使用和跨平台的C++应用程序开发框架,被广泛应用于图形用户界面、网络应用程序、游戏、嵌入式系统等各种领域。QT5.12.6的下载地址:cid:link_0打开下载链接后选择下面的版本进行下载:qt-opensource-windows-x86-5.12.6.exe 13-Nov-2019 07:28 3.7G Details软件安装时断网安装,否则会提示输入账户。安装的时候,第一个复选框里勾选一个mingw 32编译器即可,其他的不管默认就行,直接点击下一步继续安装。三、设计过程【1】环境介绍我的开发环境:QT的版本是5.12.6 、编译器MinGW32位(这与编译器没多大关系)。当前的项目代码是采用标准的Qt框架编写,没有依赖其他任何第三方库,只是用到了Qt里的网络通信模块。MQTT协议是根据MQTT官方文档,自己进行协议封包,利用QT网络通信模块建立TCP协议完成与物联网平台的通信。 而Qt本身也是跨平台的框架,所以,这份代码可以运行在Android、IOS、windows、Linux、macOS等等环境下。 通过这份项目的学习可以充分搞清楚什么是MQTT协议,了解协议如何封包、解包等过程。【2】整体的项目工程【3】MQTT协议登录过程/* 函数功能: 登录服务器 函数返回值: 0表示成功 1表示失败 */ quint8 MQTT_WorkClass::MQTT_Connect(char *ClientID,char *Username,char *Password) { quint8 i,j; int ClientIDLen = strlen(ClientID); int UsernameLen = strlen(Username); int PasswordLen = strlen(Password); int DataLen; mqtt_txlen=0; //可变报头+Payload 每个字段包含两个字节的长度标识 DataLen = 10 + (ClientIDLen+2) + (UsernameLen+2) + (PasswordLen+2); //固定报头 //控制报文类型 mqtt_txbuf[mqtt_txlen++] = 0x10; //MQTT Message Type CONNECT //剩余长度(不包括固定头部) do { quint8 encodedByte = DataLen % 128; DataLen = DataLen / 128; // if there are more data to encode, set the top bit of this byte if ( DataLen > 0 ) encodedByte = encodedByte | 128; mqtt_txbuf[mqtt_txlen++] = encodedByte; }while ( DataLen > 0 ); //可变报头 //协议名 mqtt_txbuf[mqtt_txlen++] = 0; // Protocol Name Length MSB mqtt_txbuf[mqtt_txlen++] = 4; // Protocol Name Length LSB mqtt_txbuf[mqtt_txlen++] = 'M'; // ASCII Code for M mqtt_txbuf[mqtt_txlen++] = 'Q'; // ASCII Code for Q mqtt_txbuf[mqtt_txlen++] = 'T'; // ASCII Code for T mqtt_txbuf[mqtt_txlen++] = 'T'; // ASCII Code for T //协议级别 mqtt_txbuf[mqtt_txlen++] = 4; // MQTT Protocol version = 4 对于 3.1.1 版协议,协议级别字段的值是 4(0x04) //连接标志 mqtt_txbuf[mqtt_txlen++] = 0xc2; // conn flags mqtt_txbuf[mqtt_txlen++] = 0; // Keep-alive Time Length MSB mqtt_txbuf[mqtt_txlen++] = 100; // Keep-alive Time Length LSB 100S心跳包 保活时间 mqtt_txbuf[mqtt_txlen++] = BYTE1(ClientIDLen);// Client ID length MSB mqtt_txbuf[mqtt_txlen++] = BYTE0(ClientIDLen);// Client ID length LSB memcpy(&mqtt_txbuf[mqtt_txlen],ClientID,ClientIDLen); mqtt_txlen += ClientIDLen; if(UsernameLen > 0) { mqtt_txbuf[mqtt_txlen++] = BYTE1(UsernameLen); //username length MSB mqtt_txbuf[mqtt_txlen++] = BYTE0(UsernameLen); //username length LSB memcpy(&mqtt_txbuf[mqtt_txlen],Username,UsernameLen); mqtt_txlen += UsernameLen; } if(PasswordLen > 0) { mqtt_txbuf[mqtt_txlen++] = BYTE1(PasswordLen); //password length MSB mqtt_txbuf[mqtt_txlen++] = BYTE0(PasswordLen); //password length LSB memcpy(&mqtt_txbuf[mqtt_txlen],Password,PasswordLen); mqtt_txlen += PasswordLen; } //清空数据 memset(mqtt_rxbuf,0,mqtt_rxlen); ReadData.clear(); MQTT_SendBuf(mqtt_txbuf,mqtt_txlen); //开始事件循环 StartEvenLoop(); if(ReadData.length()==0) { //开始事件循环 StartEvenLoop(); } memcpy((char *)mqtt_rxbuf,ReadData.data(),ReadData.length()); //CONNECT if(mqtt_rxbuf[0]==parket_connetAck[0] && mqtt_rxbuf[1]==parket_connetAck[1]) //连接成功 { return 0;//连接成功 } return 1; }【4】MQTT取消订阅与订阅主题/* 函数功能: MQTT订阅/取消订阅数据打包函数 函数参数: topic 主题 qos 消息等级 0:最多分发一次 1: 至少分发一次 2: 仅分发一次 whether 订阅/取消订阅请求包 (1表示订阅,0表示取消订阅) 返回值: 0表示成功 1表示失败 */ quint8 MQTT_WorkClass::MQTT_SubscribeTopic(char *topic,quint8 qos,quint8 whether) { quint8 i,j; mqtt_txlen=0; int topiclen = strlen(topic); int DataLen = 2 + (topiclen+2) + (whether?1:0);//可变报头的长度(2字节)加上有效载荷的长度 //固定报头 //控制报文类型 if(whether)mqtt_txbuf[mqtt_txlen++] = 0x82; //消息类型和标志订阅 else mqtt_txbuf[mqtt_txlen++] = 0xA2; //取消订阅 //剩余长度 do { quint8 encodedByte = DataLen % 128; DataLen = DataLen / 128; // if there are more data to encode, set the top bit of this byte if ( DataLen > 0 ) encodedByte = encodedByte | 128; mqtt_txbuf[mqtt_txlen++] = encodedByte; }while ( DataLen > 0 ); //可变报头 mqtt_txbuf[mqtt_txlen++] = 0; //消息标识符 MSB mqtt_txbuf[mqtt_txlen++] = 0x0A; //消息标识符 LSB //有效载荷 mqtt_txbuf[mqtt_txlen++] = BYTE1(topiclen);//主题长度 MSB mqtt_txbuf[mqtt_txlen++] = BYTE0(topiclen);//主题长度 LSB memcpy(&mqtt_txbuf[mqtt_txlen],topic,topiclen); mqtt_txlen += topiclen; if(whether) { mqtt_txbuf[mqtt_txlen++] = qos;//QoS级别 } ReadData.clear(); MQTT_SendBuf(mqtt_txbuf,mqtt_txlen); //开始事件循环 StartEvenLoop(); if(ReadData.length()==0) { //开始事件循环 StartEvenLoop(); } memcpy((char *)mqtt_rxbuf,ReadData.data(),ReadData.length()); if(mqtt_rxbuf[0]==parket_subAck[0] && mqtt_rxbuf[1]==parket_subAck[1]) //订阅成功 { return 0;//订阅成功 } return 1; //失败 }【5】MQTT主题数据发布//MQTT发布数据打包函数 //topic 主题 //message 消息 //qos 消息等级 quint8 MQTT_WorkClass::MQTT_PublishData(char *topic, char *message, quint8 qos) { int topicLength = strlen(topic); int messageLength = strlen(message); static quint16 id=0; int DataLen; mqtt_txlen=0; //有效载荷的长度这样计算:用固定报头中的剩余长度字段的值减去可变报头的长度 //QOS为0时没有标识符 //数据长度 主题名 报文标识符 有效载荷 if(qos) DataLen = (2+topicLength) + 2 + messageLength; else DataLen = (2+topicLength) + messageLength; //固定报头 //控制报文类型 这是发送包第1个字节 //报文的字节位:  7654 3 21 0 /* 第7、6、5、4位 表示是PUBLISH 报文 固定填0011 第3位 是DUP位,重发标志,如果是重发这个位就需要置位1,第一次发生置位0 主要是客户端重发时,防止服务器重复收到多个相同的消息 第2、1位 QoS等级. 分别可以填 00 01 10 ---对应就是0 1 2 第0位 是RETAIN位, 如果客户端发给服务端的 PUBLISH 报文的保留(RETAIN) 标志被设置为 1, 服务端必须存储这个应用消息和它的服务质量等级(QoS) */ //注意: 我这里没有增加DUP位的设置,如果要增加,可以给当前函数增加一个形参 //标识当前发送是第一次发送,还是重复发送 if(qos==0) { //0011 0000 mqtt_txbuf[mqtt_txlen++] = 0x30; // MQTT Message Type PUBLISH } else if(qos==1) { //0011 0011 mqtt_txbuf[mqtt_txlen++] = 0x33; // MQTT Message Type PUBLISH } else if(qos==2) { //0011 0101 mqtt_txbuf[mqtt_txlen++] = 0x35; // MQTT Message Type PUBLISH } LogSend(QString("消息质量等级:d78bfb9-77b6-4fc3-a66c-4fa4414a7687n").arg(qos)); //剩余长度---这是第2个字节 do { quint8 encodedByte = DataLen % 128; DataLen = DataLen / 128; // if there are more data to encode, set the top bit of this byte if ( DataLen > 0 ) encodedByte = encodedByte | 128; mqtt_txbuf[mqtt_txlen++] = encodedByte; }while ( DataLen > 0 ); // qDebug()<<"mqtt_txlen:"<<mqtt_txlen; //3 //主题名称1、2 字节 mqtt_txbuf[mqtt_txlen++] = BYTE1(topicLength);//主题长度MSB mqtt_txbuf[mqtt_txlen++] = BYTE0(topicLength);//主题长度LSB memcpy(&mqtt_txbuf[mqtt_txlen],topic,topicLength);//拷贝主题 mqtt_txlen += topicLength; //报文标识符 if(qos) { //第6、7字节 mqtt_txbuf[mqtt_txlen++] = BYTE1(id); mqtt_txbuf[mqtt_txlen++] = BYTE0(id); id++; } memcpy(&mqtt_txbuf[mqtt_txlen],message,messageLength); mqtt_txlen += messageLength; ReadData.clear(); MQTT_SendBuf(mqtt_txbuf,mqtt_txlen); //如果订阅了主题 // 订阅者:设备 平台下发消息给设备 // 这里就可以收到MQTT服务器的响应返回值 // QOS==1 服务器响应: "P\x02\x00\x07" // QOS==2 服务器响应: "@\x02\x00\x18" 最后一个是次数 //开始事件循环 StartEvenLoop(); return mqtt_txlen; }六、总结物联网是通过约定的协议将原本独立存在的设备相互连接起来,并最终实现智能识别、定位、跟踪、监测、控制和管理的一种网络,无需人与人、或人与设备的互动。通俗来说物联网就是“物物相连的网”,主要应用于智能交通、智能医疗、智能家居、智能物流、智能电力等领域。目前物联网产业正在飞快发展着,从智能电视、智能家居、智能汽车、医疗健康、智能玩具、机器人等延伸到可穿戴设备领域。 物联网将赋能智能硬件向多元的消费场景渗透,从而创造更加便捷、舒适、安全、节能的生活环境。以智能家居为例,我们借助物联网可远程控制家庭里面的每一件智能家居,像电灯,电视,空调等,给我们的生活带来更多的便利。随着5G 技术的快速发展,物联网将应用到更多领域,由此可见物联网产业仍具有较大的发展空间。
  • [技术干货] 利用ffmpeg获取媒体文件详细信息
    一、前言做音视频开发过程中,经常需要获取媒体文件的详细信息。比如:获取视频文件的总时间、帧率、尺寸、码率等等信息。 获取音频文件的的总时间、帧率、码率,声道等信息。 这篇文章贴出2个我封装好的函数,直接调用就能获取媒体信息返回,copy过去就能使用,非常方便。如果要获取详细信息,可以使用ffprobe实现,也可以调用ffmpeg函数直接打开视频解析获取。下面会演示两种方式,一种直接调用 ffprobe.exe实现,一种是调用ffmpeg函数直接打开视频解析获取。如果调用ffprobe.exe实现,可以编译ffmpeg源码,以静态方式编译ffprobe.exe,这样调用起来比较方便,不需要带任何的依赖库。下面 调用ffprobe.exe以JSON形式输出媒体文件的详细信息。ffprobe -v quiet -of json -i D:/123.mp4 -show_streams执行之后直接通过JSON格式输出:C:\Users\11266>ffprobe -v quiet -of json -i D:/123.mp4 -show_streams { "streams": [ { "index": 0, "codec_name": "aac", "codec_long_name": "AAC (Advanced Audio Coding)", "profile": "LC", "codec_type": "audio", "codec_time_base": "1/88200", "codec_tag_string": "mp4a", "codec_tag": "0x6134706d", "sample_fmt": "fltp", "sample_rate": "88200", "channels": 2, "channel_layout": "stereo", "bits_per_sample": 0, "r_frame_rate": "0/0", "avg_frame_rate": "0/0", "time_base": "1/44100", "start_pts": 0, "start_time": "0.000000", "duration_ts": 4141046, "duration": "93.901270", "bit_rate": "127948", "max_bit_rate": "132760", "nb_frames": "4045", "disposition": { "default": 1, "dub": 0, "original": 0, "comment": 0, "lyrics": 0, "karaoke": 0, "forced": 0, "hearing_impaired": 0, "visual_impaired": 0, "clean_effects": 0, "attached_pic": 0, "timed_thumbnails": 0 }, "tags": { "creation_time": "2015-04-30T02:43:22.000000Z", "language": "und", "handler_name": "GPAC ISO Audio Handler" } }, { "index": 1, "codec_name": "h264", "codec_long_name": "H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10", "profile": "Main", "codec_type": "video", "codec_time_base": "2349/70450", "codec_tag_string": "avc1", "codec_tag": "0x31637661", "width": 1280, "height": 720, "coded_width": 1280, "coded_height": 720, "has_b_frames": 0, "sample_aspect_ratio": "1:1", "display_aspect_ratio": "16:9", "pix_fmt": "yuv420p", "level": 51, "chroma_location": "left", "refs": 1, "is_avc": "true", "nal_length_size": "4", "r_frame_rate": "25/1", "avg_frame_rate": "35225/2349", "time_base": "1/30000", "start_pts": 0, "start_time": "0.000000", "duration_ts": 2816400, "duration": "93.880000", "bit_rate": "582474", "bits_per_raw_sample": "8", "nb_frames": "1409", "disposition": { "default": 1, "dub": 0, "original": 0, "comment": 0, "lyrics": 0, "karaoke": 0, "forced": 0, "hearing_impaired": 0, "visual_impaired": 0, "clean_effects": 0, "attached_pic": 0, "timed_thumbnails": 0 }, "tags": { "creation_time": "2015-04-30T02:43:23.000000Z", "language": "und", "handler_name": "GPAC ISO Video Handler" } } ] }如果只是想要得到 媒体的总时长、尺寸信息,那么执行下面命令即可:C:\Users\11266>ffprobe -i D:/123.mp4 ffprobe version 4.2.2 Copyright (c) 2007-2019 the FFmpeg developers built with gcc 9.2.1 (GCC) 20200122 configuration: --disable-static --enable-shared --enable-gpl --enable-version3 --enable-sdl2 --enable-fontconfig --enable-gnutls --enable-iconv --enable-libass --enable-libdav1d --enable-libbluray --enable-libfreetype --enable-libmp3lame --enable-libopencore-amrnb --enable-libopencore-amrwb --enable-libopenjpeg --enable-libopus --enable-libshine --enable-libsnappy --enable-libsoxr --enable-libtheora --enable-libtwolame --enable-libvpx --enable-libwavpack --enable-libwebp --enable-libx264 --enable-libx265 --enable-libxml2 --enable-libzimg --enable-lzma --enable-zlib --enable-gmp --enable-libvidstab --enable-libvorbis --enable-libvo-amrwbenc --enable-libmysofa --enable-libspeex --enable-libxvid --enable-libaom --enable-libmfx --enable-amf --enable-ffnvcodec --enable-cuvid --enable-d3d11va --enable-nvenc --enable-nvdec --enable-dxva2 --enable-avisynth --enable-libopenmpt libavutil 56. 31.100 / 56. 31.100 libavcodec 58. 54.100 / 58. 54.100 libavformat 58. 29.100 / 58. 29.100 libavdevice 58. 8.100 / 58. 8.100 libavfilter 7. 57.100 / 7. 57.100 libswscale 5. 5.100 / 5. 5.100 libswresample 3. 5.100 / 3. 5.100 libpostproc 55. 5.100 / 55. 5.100 Input #0, mov,mp4,m4a,3gp,3g2,mj2, from 'D:/123.mp4': Metadata: major_brand : mp42 minor_version : 0 compatible_brands: mp42isom creation_time : 2015-04-30T02:43:22.000000Z Duration: 00:01:33.90, start: 0.000000, bitrate: 715 kb/s Stream #0:0(und): Audio: aac (LC) (mp4a / 0x6134706D), 88200 Hz, stereo, fltp, 127 kb/s (default) Metadata: creation_time : 2015-04-30T02:43:22.000000Z handler_name : GPAC ISO Audio Handler Stream #0:1(und): Video: h264 (Main) (avc1 / 0x31637661), yuv420p, 1280x720 [SAR 1:1 DAR 16:9], 582 kb/s, 15 fps, 25 tbr, 30k tbn, 20000k tbc (default) Metadata: creation_time : 2015-04-30T02:43:23.000000Z handler_name : GPAC ISO Video Handler二、调用ffprobe获取媒体信息下面利用Qt编写代码调用ffprobe可执行文件,解析媒体信息输出。下面封装了2个函数,完整媒体信息的解析返回。【1】获取尺寸和时长//媒体信息 struct MEDIA_INFO { int width; //宽度 int height; //高度 qint64 duration;//视频总时长--毫秒 }; //获取视频的尺寸和总时间信息 struct MEDIA_INFO GetVideo_SizeInfo(QString file) { int w, h; struct MEDIA_INFO info = {0,0,0}; //拼接ffmpge的路径 QString cmd = QString("%1 -i \"-07e2b72-d5f3-497f-98d9-a12cdb25f805"").arg(FFPROBE_NAME).arg(file); QProcess process; process.setProcessChannelMode(QProcess::MergedChannels); process.start(cmd.toUtf8()); process.waitForFinished(); process.waitForReadyRead(); //qDebug() << "cmd:" << cmd; if (process.exitCode() == 0) { log_printf(QString("Run Success")); QString qba = process.readAll(); QByteArray utf8_str = qba.toUtf8(); // Match duration QRegularExpression reDuration("Duration: (\\d{2}:\\d{2}:\\d{2}\\.\\d{2})"); QRegularExpressionMatch matchDuration = reDuration.match(utf8_str); if (matchDuration.hasMatch()) { QString duration = matchDuration.captured(1); // "00:06:37.15" qDebug() << "视频总时间:" << duration; int hour=duration.section(":", 0, 0).toInt(); int minute = duration.section(":", 1, 1).toInt(); int second = duration.section(":", 2, 3).section(".",0,0).toInt(); int ms = duration.section(":", 2, 3).section(".", 1, 1).toInt(); info.duration= hour * 60 * 60 *1000 + minute * 60 *1000 + second*1000 + ms; } else { qDebug() << "No duration match found."; } // Match resolution QRegularExpression reResolution("\\d{3,4}x\\d{3,4}"); QRegularExpressionMatch matchResolution = reResolution.match(utf8_str); if (matchResolution.hasMatch()) { QString resolution = matchResolution.captured(0); //qDebug() << "视频尺寸:" << resolution; //qDebug() << "视频尺寸--w:" << resolution.section("x", 0, 0); //qDebug() << "视频尺寸--h:" << resolution.section("x", 1, 1); info.width = resolution.section("x", 0, 0).toInt(); info.height = resolution.section("x", 1, 1).toInt(); } else { qDebug() << "No resolution match found."; } } else { log_printf(QString("Run ERROR")); return info; } return info; }【2】获取媒体详细并解析出来// 定义用于存储解析结果的结构体 struct Stream { int index; QString codecName; QString codecLongName; QString profile; QString codecType; QString codecTimeBase; QString codecTagString; QString codecTag; int width; int height; int codedWidth; int codedHeight; bool hasBFrames; QString pixFmt; int level; QString colorRange; QString colorSpace; QString colorTransfer; QString colorPrimaries; QString chromaLocation; int refs; bool isAVC; int nalLengthSize; QString rFrameRate; QString avgFrameRate; QString timeBase; qint64 startPts; QString startTime; qint64 durationTs; QString duration; int bitRate; int bitsPerRawSample; int nbFrames; struct Disposition { int defaultValue; int dub; int original; int comment; int lyrics; int karaoke; int forced; int hearingImpaired; int visualImpaired; int cleanEffects; int attachedPic; int timedThumbnails; } disposition; struct Tags { QString language; QString handlerName; } tags; }; //解析存放媒体信息的JSON结构 QVector<Stream> DecodeMediaInfo(QString mediafile) { QByteArray byte_data=mediafile.toUtf8(); //获取媒体信息 QByteArray jsonStr = GetMediaInfo(byte_data.data()); // 将json字符串转换为json文档对象 QJsonDocument doc = QJsonDocument::fromJson(jsonStr); // 获取顶层json对象 QJsonObject topLevelObj = doc.object(); // 获取streams数组 QJsonArray streamsArray = topLevelObj.value("streams").toArray(); // 遍历streams数组,将每个元素转换为Stream结构体 QVector<Stream> streamVec; for (const QJsonValue & streamValue : streamsArray) { QJsonObject streamObj = streamValue.toObject(); // 创建新的Stream实例,并设置属性值 Stream stream; stream.index = streamObj.value("index").toInt(); stream.codecName = streamObj.value("codec_name").toString(); stream.codecLongName = streamObj.value("codec_long_name").toString(); stream.profile = streamObj.value("profile").toString(); stream.codecType = streamObj.value("codec_type").toString(); stream.codecTimeBase = streamObj.value("codec_time_base").toString(); stream.codecTagString = streamObj.value("codec_tag_string").toString(); stream.codecTag = streamObj.value("codec_tag").toString(); stream.width = streamObj.value("width").toInt(); stream.height = streamObj.value("height").toInt(); stream.codedWidth = streamObj.value("coded_width").toInt(); stream.codedHeight = streamObj.value("coded_height").toInt(); stream.hasBFrames = streamObj.value("has_b_frames").toBool(); stream.pixFmt = streamObj.value("pix_fmt").toString(); stream.level = streamObj.value("level").toInt(); stream.colorRange = streamObj.value("color_range").toString(); stream.colorSpace = streamObj.value("color_space").toString(); stream.colorTransfer = streamObj.value("color_transfer").toString(); stream.colorPrimaries = streamObj.value("color_primaries").toString(); stream.chromaLocation = streamObj.value("chroma_location").toString(); stream.refs = streamObj.value("refs").toInt(); stream.isAVC = streamObj.value("is_avc").toBool(); stream.nalLengthSize = streamObj.value("nal_length_size").toInt(); stream.rFrameRate = streamObj.value("r_frame_rate").toString(); stream.avgFrameRate = streamObj.value("avg_frame_rate").toString(); stream.timeBase = streamObj.value("time_base").toString(); stream.startPts = streamObj.value("start_pts").toInt(); stream.startTime = streamObj.value("start_time").toString(); stream.durationTs = streamObj.value("duration_ts").toInt(); stream.duration = streamObj.value("duration").toString(); stream.bitRate = streamObj.value("bit_rate").toInt(); stream.bitsPerRawSample = streamObj.value("bits_per_raw_sample").toInt(); stream.nbFrames = streamObj.value("nb_frames").toInt(); // 解析disposition对象 QJsonObject dispositionObj = streamObj.value("disposition").toObject(); stream.disposition.defaultValue = dispositionObj.value("default").toInt(); stream.disposition.dub = dispositionObj.value("dub").toInt(); stream.disposition.original = dispositionObj.value("original").toInt(); stream.disposition.comment = dispositionObj.value("comment").toInt(); stream.disposition.lyrics = dispositionObj.value("lyrics").toInt(); stream.disposition.karaoke = dispositionObj.value("karaoke").toInt(); stream.disposition.forced = dispositionObj.value("forced").toInt(); stream.disposition.hearingImpaired = dispositionObj.value("hearing_impaired").toInt(); stream.disposition.visualImpaired = dispositionObj.value("visual_impaired").toInt(); stream.disposition.cleanEffects = dispositionObj.value("clean_effects").toInt(); stream.disposition.attachedPic = dispositionObj.value("attached_pic").toInt(); stream.disposition.timedThumbnails = dispositionObj.value("timed_thumbnails").toInt(); // 解析tags对象 QJsonObject tagsObj = streamObj.value("tags").toObject(); stream.tags.language = tagsObj.value("language").toString(); stream.tags.handlerName = tagsObj.value("handler_name").toString(); // 将Stream实例添加到vector中 streamVec.append(stream); // 打印解析结果 for (const Stream & stream : streamVec) { qDebug() << "Index:" << stream.index << "Codec Name:" << stream.codecName << "Codec Long Name:" << stream.codecLongName << "Profile:" << stream.profile << "Codec Type:" << stream.codecType << "Codec Time Base:" << stream.codecTimeBase << "Codec Tag String:" << stream.codecTagString << "Codec Tag:" << stream.codecTag << "Width:" << stream.width << "Height:" << stream.height << "Coded Width:" << stream.codedWidth << "Coded Height:" << stream.codedHeight << "Has B Frames:" << stream.hasBFrames << "Pixel Format:" << stream.pixFmt << "Level:" << stream.level << "Color Range:" << stream.colorRange << "Color Space:" << stream.colorSpace << "Color Transfer:" << stream.colorTransfer << "Color Primaries:" << stream.colorPrimaries << "Chroma Location:" << stream.chromaLocation << "Refs:" << stream.refs << "Is AVC:" << stream.isAVC << "NAL Length Size:" << stream.nalLengthSize << "R Frame Rate:" << stream.rFrameRate << "Avg Frame Rate:" << stream.avgFrameRate << "Time Base:" << stream.timeBase << "Start PTS:" << stream.startPts << "Start Time:" << stream.startTime << "Duration TS:" << stream.durationTs << "Duration:" << stream.duration << "Bitrate:" << stream.bitRate << "Bits per Raw Sample:" << stream.bitsPerRawSample << "Number of Frames:" << stream.nbFrames << "Disposition Default Value:" << stream.disposition.defaultValue << "Disposition Dub:" << stream.disposition.dub << "Disposition Original:" << stream.disposition.original << "Disposition Comment:" << stream.disposition.comment << "Disposition Lyrics:" << stream.disposition.lyrics << "Disposition Karaoke:" << stream.disposition.karaoke << "Disposition Forced:" << stream.disposition.forced << "Disposition Hearing Impaired:" << stream.disposition.hearingImpaired << "Disposition Visual Impaired:" << stream.disposition.visualImpaired << "Disposition Clean Effects:" << stream.disposition.cleanEffects << "Disposition Attached Pic:" << stream.disposition.attachedPic << "Disposition Timed Thumbnails:" << stream.disposition.timedThumbnails << "Tags Language:" << stream.tags.language << "Tags Handler Name:" << stream.tags.handlerName; } } return streamVec; }三、调用ffmpeg函数获取媒体信息如果在程序里不方便调用ffprobe.exe,那么也可以直接调用ffmpeg的函数,打开视频、音频解析媒体数据。【1】获取视频信息下面给出代码:#include <libavcodec/avcodec.h> #include <libavformat/avformat.h> #include <libavutil/dict.h> int main() { AVFormatContext *format_ctx = NULL; int ret; // 打开视频文件 ret = avformat_open_input(&format_ctx, "video.mp4", NULL, NULL); if (ret != 0) { printf("无法打开视频文件\n"); return -1; } // 获取视频文件中每个流的详细信息 ret = avformat_find_stream_info(format_ctx, NULL); if (ret < 0) { printf("无法获取视频流信息\n"); return -1; } // 输出视频流的详细信息 for (int i = 0; i < format_ctx->nb_streams; i++) { AVStream *stream = format_ctx->streams[i]; AVCodecParameters *params = stream->codecpar; AVRational time_base = stream->time_base; printf("流%d:\n", i); printf(" 时间基数:%d/Ý07e2b72-d5f3-497f-98d9-a12cdb25f805n", time_base.num, time_base.den); printf(" 编码器ID:Ý07e2b72-d5f3-497f-98d9-a12cdb25f805n", params->codec_id); printf(" 视频宽度:Ý07e2b72-d5f3-497f-98d9-a12cdb25f805n", params->width); printf(" 视频高度:Ý07e2b72-d5f3-497f-98d9-a12cdb25f805n", params->height); printf(" 帧率:%d/Ý07e2b72-d5f3-497f-98d9-a12cdb25f805n", stream->avg_frame_rate.num, stream->avg_frame_rate.den); } // 获取元数据(metadata) AVDictionaryEntry *tag = NULL; while ((tag = av_dict_get(format_ctx->metadata, "", tag, AV_DICT_IGNORE_SUFFIX))) { printf("%s=%s\n", tag->key, tag->value); } // 关闭输入文件 avformat_close_input(&format_ctx); return 0; }这个代码片段可以在Linux或Windows操作系统上编译,并且需要在编译时链接FFmpeg库。【2】获取视频、音频详细信息#include <stdio.h> #include <libavformat/avformat.h> int main(int argc, char **argv) { AVFormatContext *fmt_ctx = NULL; AVDictionaryEntry *tag = NULL; // 打开输入媒体文件 if (avformat_open_input(&fmt_ctx, argv[1], NULL, NULL) < 0) { fprintf(stderr, "Cannot open input file\n"); return -1; } // 获取媒体文件信息 if (avformat_find_stream_info(fmt_ctx, NULL) < 0) { fprintf(stderr, "Cannot find stream information\n"); avformat_close_input(&fmt_ctx); return -1; } // 输出媒体文件信息 printf("File: %s\n", argv[1]); printf("Format: %s\n", fmt_ctx->iformat->name); printf("Duration: %lld seconds\n", fmt_ctx->duration / AV_TIME_BASE); for (int i = 0; i < fmt_ctx->nb_streams; i++) { AVStream *stream = fmt_ctx->streams[i]; const char *type = "Unknown"; if (stream->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) { type = "Video"; printf("\n%s Stream #%d:\n", type, i); printf("Codec: %s\n", avcodec_get_name(stream->codecpar->codec_id)); printf("Resolution: %dxÝ07e2b72-d5f3-497f-98d9-a12cdb25f805n", stream->codecpar->width, stream->codecpar->height); printf("Frame Rate: %.2f fps\n", av_q2d(stream->avg_frame_rate)); printf("Bit Rate: %lld kbps\n", stream->codecpar->bit_rate / 1000); } else if (stream->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) { type = "Audio"; printf("\n%s Stream #%d:\n", type, i); printf("Codec: %s\n", avcodec_get_name(stream->codecpar->codec_id)); printf("Sample Rate: %d Hz\n", stream->codecpar->sample_rate); printf("Channels: Ý07e2b72-d5f3-497f-98d9-a12cdb25f805n", stream->codecpar->channels); printf("Bit Rate: %lld kbps\n", stream->codecpar->bit_rate / 1000); } // 输出流的元数据信息 while ((tag = av_dict_get(stream->metadata, "", tag, AV_DICT_IGNORE_SUFFIX))) { printf("%s=%s\n", tag->key, tag->value); } } // 关闭输入媒体文件 avformat_close_input(&fmt_ctx); return 0; }使用方法:将示例代码保存为ffprobe.c文件。在命令行中进入该文件所在目录,执行以下命令进行编译:gcc -o ffprobe ffprobe.c -lavformat -lavcodec -lavutil执行以下命令获取视频或音频文件的全部参数信息:./ffprobe [input_file]其中,[input_file]是输入的视频或音频文件路径。例如,执行以下命令获取test.mp4视频文件的全部参数信息:./ffprobe test.mp4
总条数:501 到第
上滑加载中