• [技术干货] 基于单片机的智能小车设计
    一、项目介绍随着科技的发展,智能机器人在日常生活中的应用越来越广泛。智能小车作为智能机器人的一种,具有便携性和多功能的特点,在教育、娱乐和工业等领域得到了广泛关注和应用。智能小车可以通过远程控制实现各种动作,如前进、后退、转弯等,并且可以通过搭载传感器实现避障、测距等功能。智能小车是一种通过采用主控芯片、蓝牙模块、电机驱动以及传感器等组件实现远程控制和避障功能的机器人。当前文章介绍基于STC89C52单片机的智能小车设计方案,提供详细的硬件和软件设计内容。二、设计方案2.1 硬件设计【1】主控芯片选择选择STC89C52单片机作为智能小车的主控芯片。单片机有广泛的应用支持,能够满足小车的控制需求。【2】显示屏选型为了显示小车的状态信息,选用LCD1602液晶显示屏。能够提供简洁明了的显示界面,并且与STC89C52单片机兼容良好。【3】通信模块选择由于需要通过手机APP远程控制小车,选择HC-05蓝牙模块进行通信。该模块易于使用、价格适中,并且与大多数手机兼容。【4】电机驱动为了控制小车的运动,采用L298N电机驱动模块。这种模块具有高性能、稳定可靠的特点,能够驱动直流电机实现小车的前进、后退、转弯等动作。【5】避障模块为了实现智能避障功能,在小车正前方安装两个红外壁障模块。这些模块能够检测前方障碍物,当检测到障碍物时,小车将停止运动,以避免碰撞。2.2 软件设计【1】主控程序在STC89C52单片机上编写主控程序,实现蓝牙通信的初始化、接收手机APP指令、控制电机驱动模块以及红外壁障的检测等功能。主控程序需要实时响应手机指令,并根据指令控制小车的运动。【2】手机APP开发 开发手机APP通过蓝牙与智能小车建立连接,并发送指令给小车,控制小车的移动动作。手机APP界面设计简洁直观,方便用户进行操作。2.3 小车运动控制流程【1】初始化开启电源后,主控芯片进行各个外设的初始化设置,包括蓝牙模块、LCD显示屏、电机驱动模块和红外壁障模块。【2】连接手机APP通过手机APP与蓝牙模块建立连接,确保手机与小车之间的通信畅通。【3】接收指令主控芯片接收手机APP发送的指令,根据指令判断小车前进、后退、左转弯、右转弯等动作。【4】控制电机根据接收到的指令,主控芯片通过电机驱动模块控制电机的旋转方向和速度,以实现小车的运动。【5】避障检测红外壁障模块实时检测前方障碍物,当检测到障碍物时,主控芯片停止发送电机指令,以避免碰撞。【6】显示状态通过LCD显示屏显示小车的状态信息,如电量、当前动作。三、源代码#include <reg52.h>​// 定义引脚连接sbit enA = P1^0; // 电机A使能引脚sbit in1 = P1^1; // 电机A正转引脚sbit in2 = P1^2; // 电机A反转引脚sbit enB = P1^3; // 电机B使能引脚sbit in3 = P1^4; // 电机B正转引脚sbit in4 = P1^5; // 电机B反转引脚​// 定义红外壁障引脚连接sbit obstacle1 = P2^0; // 红外壁障模块1sbit obstacle2 = P2^1; // 红外壁障模块2​// 定义LCD1602液晶显示屏引脚连接sbit rs = P3^0; // RS引脚sbit rw = P3^1; // RW引脚sbit en = P3^2; // EN引脚sbit lcd_d4 = P3^4; // 数据线D4引脚sbit lcd_d5 = P3^5; // 数据线D5引脚sbit lcd_d6 = P3^6; // 数据线D6引脚sbit lcd_d7 = P3^7; // 数据线D7引脚​// 初始化LCD1602液晶显示屏void LCD_Init();// 打印字符串到LCD1602液晶显示屏指定位置void LCD_PrintString(unsigned char row, unsigned char col, char *str);// 发送命令到LCD1602液晶显示屏void LCD_SendCommand(unsigned char command);// 发送数据到LCD1602液晶显示屏void LCD_SendData(unsigned char data);​// 延时函数void delay(unsigned int time) { unsigned int i, j; for(i = 0; i < time; i++) for(j = 0; j < 125; j++);}​// 初始化函数void Init() { LCD_Init(); // 初始化LCD1602液晶显示屏 // 初始化其他外设,蓝牙模块、红外壁障模块}​// 控制电机A正转void MotorA_Forward() { in1 = 1; in2 = 0;}​// 控制电机A反转void MotorA_Backward() { in1 = 0; in2 = 1;}​// 控制电机A停止void MotorA_Stop() { in1 = 0; in2 = 0;}​// 控制电机B正转void MotorB_Forward() { in3 = 1; in4 = 0;}​// 控制电机B反转void MotorB_Backward() { in3 = 0; in4 = 1;}​// 控制电机B停止void MotorB_Stop() { in3 = 0; in4 = 0;}​// 检测障碍物unsigned char ObstacleDetected() { // 红外壁障检测代码,返回检测结果}​// 主程序void main() { Init(); // 初始化函数 while(1) { // 主程序的逻辑 // 检测障碍物 if (ObstacleDetected()) { // 如果检测到障碍物,停止电机运动 MotorA_Stop(); MotorB_Stop(); } }}​四、总结本设计介绍了基于STC89C52单片机的智能小车设计方案。通过与手机APP的连接,小车可以远程控制,并利用红外壁障模块实现避障功能。
  • [技术干货] 基于单片机的数字温度计设计
    一、项目背景数字温度计是一种用于测量和显示环境温度的设备。本文章介绍基于STC89C52主控芯片的数字温度计的设计过程和实现原理。该设计采用DS18B20温度传感器进行温度采集,使用LCD1602显示屏进行温度显示,通过按键设置温度的上限和下限阀值,并通过蜂鸣器进行报警。二、系统架构数字温度计的系统架构如下所示:(1)硬件部分:主控芯片STC89C52、DS18B20温度传感器、LCD1602显示屏、按键、蜂鸣器;(2)软件部分:嵌入式C语言程序。三、系统功能设计【1】温度采集:通过DS18B20温度传感器采集环境温度;【2】温度显示:使用LCD1602显示屏显示当前环境温度;【3】阈值设置:通过按键设置温度的上限和下限阀值;【4】报警功能:当温度超出阀值时,蜂鸣器发出报警信号。四、整体设计4.1 硬件设计【1】主控芯片选择:STC89C52,具有较好的性能和丰富的外设资源,适合作为数字温度计的核心处理器;【2】温度传感器:采用DS18B20温度传感器,利用其一线通信功能实现温度采集;【3】显示屏:使用LCD1602显示屏,通过并口连接到主控芯片,实时显示温度信息;【4】按键:通过按键设置温度阀值,包括上限和下限;【5】蜂鸣器:当温度超出阀值时,蜂鸣器发出报警信号。4.2 软件设计【1】GPIO配置:配置主控芯片的GPIO引脚,包括DS18B20温度传感器的引脚、LCD1602显示屏的引脚、按键的引脚和蜂鸣器的引脚;【2】DS18B20通信:利用主控芯片的IO口实现与DS18B20温度传感器的一线通信,获取温度数据;【3】LCD显示:通过并口通信协议,将温度数据发送给LCD1602显示屏进行显示;【4】按键检测:使用外部中断方式监听按键引脚的状态变化,当按键被按下时,进入设置模式,并根据按键次数调整温度阀值;【5】温度比较和报警:在主循环中,不断比较当前温度与设置的阀值,当温度超出阈值时,触发蜂鸣器报警。五、源代码#include <reg52.h>​// 定义IO口sbit DQ = P2^0;sbit RS = P2^1;sbit RW = P2^2;sbit E = P2^3;sbit K1 = P2^4;sbit K2 = P2^5;sbit Buzzer = P2^6;​// 定义全局变量unsigned int highTemp = 30; // 温度上限unsigned int lowTemp = 20; // 温度下限unsigned int currentTemp = 0; // 当前温度​// 延时函数void delay(unsigned int ms) { unsigned int i, j; for (i = ms; i > 0; i--) for (j = 110; j > 0; j--);}​// DS18B20初始化bit Init_DS18B20() { bit presence; DQ = 1; // 设置DQ为输出 delay(1); DQ = 0; // 主机拉低DQ线 delay(75); DQ = 1; // 主机释放DQ线 delay(4); presence = DQ; // 从机检测到的应答信号 delay(20); return presence;}​// DS18B20写字节void Write_DS18B20(unsigned char dat) { unsigned char i; for (i = 0; i < 8; i++) { DQ = 0; // 主机拉低DQ线 _nop_(); DQ = dat & 0x01; // 写数据位 delay(5); DQ = 1; // 主机释放DQ线 dat >>= 1; }}​// DS18B20读字节unsigned char Read_DS18B20() { unsigned char i, dat; for (i = 0; i < 8; i++) { DQ = 0; // 主机拉低DQ线 _nop_(); DQ = 1; // 主机释放DQ线 _nop_(); dat >>= 1; if (DQ) dat |= 0x80; // 读数据位 delay(5); } return dat;}​// 读取温度unsigned char ReadTemperature() { unsigned char temp_h, temp_l; Init_DS18B20(); Write_DS18B20(0xCC); // 跳过ROM指令 Write_DS18B20(0xBE); // 发送读温度命令 temp_l = Read_DS18B20(); // 读低字节 temp_h = Read_DS18B20(); // 读高字节 currentTemp = temp_h; return temp_l;}​// LCD初始化void LCD_Init() { delay(15); Write_Command(0x38); // 设置8位数据总线,2行显示,5x7点阵 Write_Command(0x0C); // 显示器打开,光标关闭 Write_Command(0x06); // 光标右移,显示器不移动 Write_Command(0x01); // 显示清屏}​// LCD写命令void Write_Command(unsigned char com) { RS = 0; RW = 0; E = 1; P0 = com; delay(1); E = 0;}​// LCD写数据void Write_Data(unsigned char dat) { RS = 1; RW = 0; E = 1; P0 = dat; delay(1); E = 0;}​// LCD显示温度void Display_Temperature(unsigned char temp) { unsigned char temp_str[5]; temp_str[0] = temp / 10 + '0'; temp_str[1] = temp % 10 + '0'; temp_str[2] = '.'; temp_str[3] = ReadTemperature() / 10 + '0'; temp_str[4] = ReadTemperature() % 10 + '0'; Write_Command(0x80); // 第一行第一个字符位置 Write_String("Temp: "); Write_Command(0x86); // 第一行第七个字符位置 Write_String(temp_str); Write_Command(0xC0); // 第二行第一个字符位置 Write_String("High: "); Write_Command(0xC6); // 第二行第七个字符位置 Write_Char(highTemp / 10 + '0'); Write_Char(highTemp % 10 + '0'); Write_Command(0xCB); // 第二行第十个字符位置 Write_String("Low: "); Write_Command(0xCF); // 第二行第十四个字符位置 Write_Char(lowTemp / 10 + '0'); Write_Char(lowTemp % 10 + '0');}​// LCD写字符串void Write_String(unsigned char *str) { while (*str != '\0') { Write_Data(*str); str++; }}​// LCD写字符void Write_Char(unsigned char dat) { Write_Data(dat);}​// 蜂鸣器报警void Alarm() { Buzzer = 0; delay(500); Buzzer = 1; delay(500);}​// 按键扫描void Key_Scan() { if (K1 == 0) { // K1按下,设置高温 delay(5); if (K1 == 0) { highTemp++; Write_Command(0xCB); // 第二行第十个字符位置 Write_Char(highTemp / 10 + '0'); Write_Char(highTemp % 10 + '0'); while (!K1); } } if (K2 == 0) { // K2按下,设置低温 delay(5); if (K2 == 0) { lowTemp--; Write_Command(0xCF); // 第二行第十四个字符位置 Write_Char(lowTemp / 10 + '0'); Write_Char(lowTemp % 10 + '0'); while (!K2); } }}​// 主函数void main() { LCD_Init(); while (1) { ReadTemperature(); // 读取温度 Display_Temperature(currentTemp); // 显示温度 if (currentTemp > highTemp || currentTemp < lowTemp) { // 温度超出阈值,触发报警 Alarm(); } Key_Scan(); // 按键扫描 }}​代码最开始定义了一些用于控制硬件的IO口,如DQ用于连接温度传感器、RS、RW、E用于连接LCD显示屏、K1、K2用于连接按键、Buzzer用于连接蜂鸣器。接下来定义了一些全局变量,包括高温上限、低温下限以及当前温度。然后是一些函数的定义和实现,包括延时函数、DS18B20温度传感器初始化函数、写字节函数、读字节函数等。ReadTemperature() 函数用于读取温度传感器的温度值,并将其保存到 currentTemp 变量中。LCD_Init() 函数用于初始化LCD显示屏。Write_Command() 和 Write_Data() 函数用于向LCD显示屏写入命令和数据。Display_Temperature() 函数用于在LCD显示屏上显示当前温度、高温上限和低温下限。Alarm() 函数用于触发蜂鸣器报警。Key_Scan() 函数用于扫描按键状态,根据按键状态来修改高温上限和低温下限。主函数 main() 中的逻辑:调用 LCD_Init() 初始化LCD显示屏。进入一个无限循环,不断读取当前温度并显示在LCD上。如果当前温度超过设定的高温上限或低于设定的低温下限,就触发报警。通过按键扫描函数来修改高温上限和低温下限。六、总结本文章详细介绍了基于STC89C52主控芯片的数字温度计的设计过程和实现原理。通过集成DS18B20温度传感器、LCD1602显示屏、按键和蜂鸣器等功能,实现了温度的采集、显示和报警功能。通过按键设置温度的上限和下限阀值,用户可以根据需要进行调整,并在超出阀值时触发报警,提醒用户注意环境温度的变化。该设计可以广泛应用于家庭、办公室和实验室等场景,为用户提供了方便、准确和实用的温度监测工具。
  • [技术干货] 基于STM32设计的智能台灯
    一、项目背景智能家居设备在现代生活中起着越来越重要的作用。智能台灯作为其中的一种,具有调节光照亮度、色温等功能,更加符合人们对于光照环境的个性化需求。当前设计一款基于STM32微控制器设计的智能台灯,该台灯具备可调节亮度和色温的特点,为用户提供了更加舒适的使用体验。二、设计目标【1】实现灯光的亮度和色温的可调节功能;【2】添加人体感应模块,实现自动开关灯;【3】实现手机远程控制灯光的功能;【4】设计简洁、稳定的硬件电路和用户友好的操作界面。三、系统架构3.1 硬件部分(1) 主控芯片:选用STM32系列微控制器,具有丰富的外设资源和强大的处理能力;(2) 电源部分:采用稳压电源模块,提供适宜电压的供电;(3) 光源部分:选择高亮度LED作为光源,配备透明灯罩,提供均匀柔和的光照;(4) 人体感应模块:采用红外传感器,检测到人体活动时自动开启灯光;(5) 无线通信模块:使用WiFi或蓝牙模块,实现手机远程控制。3.2 软件部分(1) 嵌入式软件:使用Keil MDK作为开发环境,编写嵌入式C语言程序,实现灯光亮度和色温的调节、人体感应控制等功能;(2) 手机控制端:设计并开发手机App,通过与智能台灯连接,实现远程控制灯光的功能。3.3 硬件选型说明【1】主控芯片:采用STM32F103RCT6【2】光源部分:(1) 高亮度LED:选择高亮度、节能的LED作为光源,推荐选择LED灯珠。(2) 透明灯罩:选择高透光性的材料制作灯罩,保证光照均匀柔和。【3】人体感应模块:(1) 红外传感器:选择灵敏度较高的红外传感器,能够快速、准确地检测到人体活动。(2) 光敏电阻:用于在光线不足时自动开启台灯,确保人体感应功能的正常工作。【4】无线通信模块选择HC05蓝牙模块,以便与手机设备进行通信。3.4 硬件设计【1】主控芯片选择:STM32F103RCT6,具有较高的性能和丰富的外设资源,适合作为智能台灯的核心处理器;【2】人体红外传感器:用于检测周围是否有人靠近;【3】光敏传感器:用于检测环境光的强度;【4】LED灯:作为台灯的光源,通过PWM控制其亮度;【5】HC05蓝牙模块:用于与手机APP通信,接收控制命令并发送状态信息。3.5 软件设计【1】GPIO配置:配置主控芯片的GPIO引脚,包括人体红外传感器输入引脚、光敏传感器输入引脚和LED灯控制引脚等;【2】外部中断配置:通过外部中断来监听人体红外感应引脚的状态变化,在触发时进行相应操作;【3】PWM配置:使用PWM控制LED灯的亮度,根据光敏传感器检测到的环境光强度动态调整PWM输出占空比;【4】蓝牙通信:通过UART配置HC05蓝牙模块,与手机APP建立蓝牙连接,接收控制命令并发送台灯状态信息;【5】主循环逻辑:在主循环中实时检测光敏传感器的数据以及人体红外感应引脚的状态,并根据相应条件进行台灯的开启和关闭操作;同时,检测蓝牙模块接收到的控制命令,并根据命令内容进行相应操作。四、主要功能实现【1】光照调节功能: 通过按键或旋钮操作,调节台灯光照的亮度和色温。亮度调节通过PWM控制LED的亮度,色温调节通过调节白光LED和彩光LED的相对亮度来实现。【2】人体感应控制: 采用红外传感器,检测到人体活动后自动开启灯光,一段时间内没有人活动则自动关闭。【3】远程控制功能: 手机App与智能台灯通过蓝牙通信,用户可以通过App控制灯光的开关、亮度和色温,实现远程控制功能。五、代码实现5.1 PWM波形控制LED灯亮度include "stm32f10x.h"​void PWM_Configuration(void);void Delay(__IO uint32_t nCount);​int main(void) { PWM_Configuration();​ while (1) { // 逐渐增加LED亮度 for (uint16_t i = 0; i <= 1000; i++) { TIM_SetCompare1(TIM2, i); // 设置PWM占空比,范围:0-1000 Delay(5000); // 延时一段时间 }​ // 逐渐减小LED亮度 for (uint16_t i = 1000; i > 0; i--) { TIM_SetCompare1(TIM2, i); Delay(5000); } }}​void PWM_Configuration(void) { RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);​ // 配置GPIO口 GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure);​ // 配置TIM2为PWM模式 TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure;​ TIM_TimeBaseStructure.TIM_Period = 1000; // 设置周期 TIM_TimeBaseStructure.TIM_Prescaler = 72 - 1; // 设置预分频 TIM_TimeBaseStructure.TIM_ClockDivision = 0; TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);​ TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; // PWM模式1 TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse = 0; // 初始占空比为0 TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; TIM_OC1Init(TIM2, &TIM_OCInitStructure); TIM_OC1PreloadConfig(TIM2, TIM_OCPreload_Enable);​ TIM_Cmd(TIM2, ENABLE); // 使能TIM2}​void Delay(__IO uint32_t nCount) { for (; nCount != 0; nCount--);}代码中使用了STM32的定时器TIM2和GPIOA的第0号引脚(PA0)来控制LED灯的亮度。在主函数中,通过循环逐渐增加和减小PWM的占空比,从而改变LED灯的亮度。5.2 智能台灯逻辑代码// 引入所需的库#include <stdio.h>#include <stdbool.h>​// 定义引脚和设备地址#define PIR_SENSOR_PIN 2#define LIGHT_SENSOR_PIN 3#define LED_PIN 4#define HC05_BAUD_RATE 9600​// 声明全局变量bool isPersonDetected = false;int lightIntensity = 0;​// 初始化函数void setup() { // 配置引脚模式 pinMode(PIR_SENSOR_PIN, INPUT); pinMode(LIGHT_SENSOR_PIN, INPUT); pinMode(LED_PIN, OUTPUT);​ // 初始化串口通信 Serial.begin(HC05_BAUD_RATE);}​// 主循环函数void loop() { // 检测人体红外感应 if (digitalRead(PIR_SENSOR_PIN) == HIGH) { isPersonDetected = true; } else { isPersonDetected = false; }​ // 检测光敏传感器 lightIntensity = analogRead(LIGHT_SENSOR_PIN);​ // 根据条件控制台灯 if (isPersonDetected && lightIntensity < 500) { digitalWrite(LED_PIN, HIGH); } else { digitalWrite(LED_PIN, LOW); }​ // 处理蓝牙通信 if (Serial.available()) { char command = Serial.read(); handleBluetoothCommand(command); }}​// 处理蓝牙命令函数void handleBluetoothCommand(char command) { // 处理从手机APP发送来的命令,例如控制台灯亮度 }​// 主函数int main() { setup(); // 初始化 while (1) { loop(); // 主循环 } return 0;}六、总结当前文章介绍了基于STM32F103RCT6主控芯片的智能台灯的设计过程和实现原理。通过集成人体红外感应、光敏检测和与手机APP连接的功能,实现了台灯的自动开关和亮度调节等智能化操作。通过手机APP与蓝牙模块的连接,用户可以远程控制台灯的开关、亮度和模式等,提供了更加方便和智能化的使用体验。
  • [技术干货] C语言实例_数据压缩与解压
    一、压缩与解压介绍数据压缩是通过一系列的算法和技术将原始数据转换为更紧凑的表示形式,以减少数据占用的存储空间。数据解压缩则是将压缩后的数据恢复到原始的表示形式。数据可以被压缩打包并减少空间占用的原因有以下几个方面:(1)无效数据的消除:在数据中可能存在大量冗余、重复或无效的信息。压缩算法可以通过识别和移除这些无效数据,从而减小数据的大小。(2)统计特性的利用:数据通常具有某种统计特性,例如频繁出现的模式、重复的字节序列等。压缩算法可以利用这些统计特性来编码数据,从而达到更高的压缩比率。(3)信息编码:压缩算法使用不同的编码方式来表示源数据,在保证数据可还原的前提下,使用更少的位数来表示信息。例如,Huffman编码、LZW编码等。常见的应用场景中会使用到数据压缩和解压功能,例如:(1)存储媒体:在硬盘、闪存等存储介质上,压缩可以节省存储空间,并提高存储效率。尤其在大规模的数据中心、云存储环境中,数据压缩可以显著减少存储成本。(2)网络传输:在网络通信中,压缩可以减少数据传输的带宽消耗,提高传输速度。尤其在低带宽、高延迟的网络环境中,压缩可以显著改善传输性能。(3)文件压缩:压缩工具如ZIP、RAR等常用于对文件进行打包和压缩,以减小文件的大小,便于存储和传输。这在文件传输、备份和归档中非常常见。(4)多媒体编码:音频、图像、视频等多媒体数据往往具有较高的冗余性,压缩算法可以大幅减小文件大小,例如MP3、JPEG、H.264等压缩算法。二、ZIP格式介绍ZIP是一种常见的文件压缩格式,它使用DEFLATE算法来进行数据压缩。下面是ZIP压缩的基本原理:(1)文件分块:ZIP压缩将要压缩的文件按照一定大小的块进行划分。每个块通常包含多个字节,并且可以独立地进行压缩处理。(2)压缩算法:对于每个块,ZIP使用DEFLATE算法进行压缩。DEFLATE是一种无损的压缩算法,它结合了LZ77算法和霍夫曼编码,可以有效地消除冗余并提高压缩比率。LZ77算法:遍历输入数据,寻找重复的模式(前缀)并使用指针来表示。通过将重复的模式替换为指针,可以达到数据压缩的效果。霍夫曼编码:利用字符出现的频率来设计一种更紧凑的编码方式。频率较高的字符使用较短的编码,频率较低的字符使用较长的编码。(3)数据存储:压缩后的数据以块为单位存储在ZIP文件中。每个块都包含压缩后的数据、块的元数据和校验和等信息。(4)全局文件目录:ZIP文件包含一个全局文件目录,记录了文件的结构以及每个文件的元数据。这使得ZIP文件能够存储多个文件,并确保可以正确地还原被压缩的文件。文件结构:全局文件目录记录了每个文件的名称、压缩前后的大小、压缩方法等信息。文件索引:全局文件目录还包含一个索引表,指明每个文件的起始位置和块的偏移量。通过索引表,可以快速定位并解压指定的文件块。(5)压缩率:ZIP压缩的效果取决于输入文件的特性和DEFLATE算法的实现。通常情况下,文本文件和重复性较高的内容可以获得更高的压缩比率,而二进制文件和已经过压缩的文件(如JPEG图像)则可能无法再次获得显著的压缩。ZIP压缩的好处是它广泛支持,并且可在各种操作系统和平台上使用。ZIP格式支持密码保护、文件夹结构、注释等功能,使其成为一种常用的压缩格式。三、C语言实现压缩和解压算法3.1 代码框架下面是使用C语言实现压缩和解压的代码框架(下一章再实现完整的算法):#include <stdio.h>#include <stdlib.h>​void compressFile(const char* inputFile, const char* outputFile) { FILE* input = fopen(inputFile, "rb"); FILE* output = fopen(outputFile, "wb"); if (input == NULL || output == NULL) { printf("Failed to open files\n"); return; }​ // 在这里执行压缩算法,将input文件的内容压缩,并写入output文件​ fclose(input); fclose(output);}​void decompressFile(const char* compressedFile, const char* outputFile) { FILE* input = fopen(compressedFile, "rb"); FILE* output = fopen(outputFile, "wb"); if (input == NULL || output == NULL) { printf("Failed to open files\n"); return; }​ // 在这里执行解压算法,将compressedFile文件的内容解压,并写入output文件​ fclose(input); fclose(output);}​int main() { const char* inputFile = "input.txt"; const char* compressedFile = "compressed.bin"; const char* decompressedFile = "decompressed.txt";​ // 压缩文件 compressFile(inputFile, compressedFile); printf("File compressed successfully.\n");​ // 解压文件 decompressFile(compressedFile, decompressedFile); printf("File decompressed successfully.\n");​ return 0;}上述代码只是用于说明基本思路,并未实现具体的压缩算法。需要在compressFile和decompressFile函数中实现实际的压缩和解压算法逻辑。在compressFile函数中,打开输入文件(例如input.txt),读取文件内容并进行压缩处理,最后将压缩后的数据写入到输出文件(例如compressed.bin)中。在decompressFile函数中,打开压缩文件(例如compressed.bin),读取压缩数据并进行解压处理,最后将解压后的数据写入到输出文件(例如decompressed.txt)中。可以选择使用现成的压缩算法库,如zlib、gzip等,或者自行实现一种简单的压缩算法(例如LZ77)。下面章节介绍使用LZ77算法实现压缩解压。3.2 完整的实现LZ77(Lempel-Ziv-Welch 1977)是一种基于字典的无损数据压缩算法,常用于文件压缩和网络传输中。通过利用数据中的重复片段来实现压缩,并且可以实现逐步的解压缩。LZ77算法的核心思想是使用一个滑动窗口和一个向前看缓冲区来寻找重复出现的字符串。算法从输入数据的开头开始,逐步读取数据并尝试匹配滑动窗口中已经出现过的字符串,如果找到匹配的字符串,就将其表示为(偏移,长度)的形式,并且在输出中只保留没有匹配的字符,然后向前滑动窗口和向前看缓冲区,继续下一轮匹配。如果没有找到匹配的字符串,则将当前字符作为新的字符串添加到滑动窗口,并输出它。下面是LZ77算法的详细步骤:(1)初始化滑动窗口和向前看缓冲区。(2)从输入数据中读取一个字符作为当前字符。(3)在滑动窗口中查找最长的匹配字符串,该字符串与向前看缓冲区中的部分或全部字符匹配。如果有多个匹配字符串具有相同的长度,选择最靠近滑动窗口末尾的字符串。(4)如果找到匹配字符串:记录该匹配字符串的偏移(滑动窗口中的位置)和长度。将未匹配的字符添加到输出,并将滑动窗口和向前看缓冲区更新为匹配之后的位置。(5)如果未找到匹配字符串:将当前字符作为新的字符串添加到滑动窗口。将当前字符添加到输出。将滑动窗口和向前看缓冲区更新为下一个位置。(6)重复步骤2至步骤5,直到遍历完整个输入数据。(7)输出压缩结果。LZ77算法的优点是简单易懂,实现相对容易,并且可以提供不错的压缩率。它也有一些限制,例如在处理长重复字符串时效率较低,并且可能会导致压缩结果略微变大。为了克服这些限制,通常会结合其他压缩算法(如Huffman编码)来进一步压缩LZ77的输出结果,以获得更好的压缩效果。下面使用C语言自行实现的LZ77压缩和解压算法完成压缩和解压:#include <stdio.h>#include <stdlib.h>#include <string.h>​#define MAX_WINDOW_SIZE 4096 // 窗口大小#define MAX_LOOKAHEAD_SIZE 16 // 向前看缓冲区大小​typedef struct { int offset; // 指向匹配字符串在滑动窗口中的偏移量 int length; // 匹配字符串的长度 char nextChar; // 下一个字符} Match;​void compressFile(const char* inputFile, const char* outputFile) { FILE* input = fopen(inputFile, "rb"); FILE* output = fopen(outputFile, "wb"); if (input == NULL || output == NULL) { printf("Failed to open files\n"); return; }​ unsigned char window[MAX_WINDOW_SIZE]; unsigned char lookahead[MAX_LOOKAHEAD_SIZE];​ int windowPos = 0; int lookaheadPos = 0;​ // 初始化窗口和向前看缓冲区 memset(window, 0, sizeof(window)); fread(lookahead, 1, MAX_LOOKAHEAD_SIZE, input); int bytesRead = ftell(input);​ while (bytesRead > 0) { Match longestMatch = {0, 0, lookahead[0]};​ // 在窗口中查找最长匹配 for (int i = windowPos - 1; i >= 0 && i >= windowPos - MAX_WINDOW_SIZE; --i) { int len = 0; while (len < MAX_LOOKAHEAD_SIZE && lookahead[len] == window[(i + len) % MAX_WINDOW_SIZE]) { ++len; } if (len > longestMatch.length) { longestMatch.offset = windowPos - i - 1; longestMatch.length = len; longestMatch.nextChar = lookahead[len]; } }​ // 写入最长匹配的偏移和长度 fwrite(&longestMatch, sizeof(Match), 1, output);​ // 更新窗口和向前看缓冲区 for (int i = 0; i < longestMatch.length + 1; ++i) { window[windowPos] = lookahead[i]; windowPos = (windowPos + 1) % MAX_WINDOW_SIZE; if (bytesRead > 0) { if (fread(lookahead, 1, 1, input) == 1) { bytesRead = ftell(input); } else { bytesRead = 0; } } } }​ fclose(input); fclose(output);}​void decompressFile(const char* compressedFile, const char* outputFile) { FILE* input = fopen(compressedFile, "rb"); FILE* output = fopen(outputFile, "wb"); if (input == NULL || output == NULL) { printf("Failed to open files\n"); return; }​ unsigned char window[MAX_WINDOW_SIZE]; unsigned char lookahead[MAX_LOOKAHEAD_SIZE];​ int windowPos = 0; int lookaheadPos = 0;​ // 初始化窗口和向前看缓冲区 memset(window, 0, sizeof(window)); fread(lookahead, 1, MAX_LOOKAHEAD_SIZE, input); int bytesRead = ftell(input);​ while (!feof(input)) { Match match;​ // 从压缩文件读取匹配信息 fread(&match, sizeof(Match), 1, input);​ // 从窗口中复制匹配字符串到输出文件 for (int i = 0; i < match.length; ++i) { unsigned char ch = window[(windowPos - match.offset + i) % MAX_WINDOW_SIZE]; fwrite(&ch, 1, 1, output); }​ // 写入下一个字符 fwrite(&match.nextChar, 1, 1, output);​ // 更新窗口和向前看缓冲区 for (int i = 0; i < match.length + 1; ++i) { window[windowPos] = match.nextChar; windowPos = (windowPos + 1) % MAX_WINDOW_SIZE; if (bytesRead > 0) { if (fread(lookahead, 1, 1, input) == 1) { bytesRead = ftell(input); } else { bytesRead = 0; } } } }​ fclose(input); fclose(output);}​int main() { const char* inputFile = "input.txt"; const char* compressedFile = "compressed.bin"; const char* decompressedFile = "decompressed.txt";​ // 压缩文件 compressFile(inputFile, compressedFile); printf("File compressed successfully.\n");​ // 解压文件 decompressFile(compressedFile, decompressedFile); printf("File decompressed successfully.\n");​ return 0;}上面代码里实现了LZ77压缩和解压算法。在压缩过程中,通过读取输入文件并根据滑动窗口中的匹配信息,将最长匹配的偏移和长度写入到输出文件。在解压过程中,从压缩文件中读取匹配信息,并根据偏移和长度将匹配的字符串复制到输出文件中。
  • [技术干货] C语言实例_解析GPS源数据
    一、GPS数据格式介绍GPS(全球定位系统)数据格式常见的是NMEA 0183格式,NMEA 0183格式是一种用于导航设备间传输数据的标准格式,定义了一套规范,使得不同厂商的设备可以通过串行通信接口(常见的是RS-232)进行数据交换。这个标准最初由美国航海电子协会(National Marine Electronics Association,简称NMEA)在1980年推出,并被广泛应用于全球的导航系统。NMEA 0183格式的数据通常以ASCII字符流的形式传输,每条数据都以$开始,以回车符(\r)和换行符(\n)结束。数据被分为不同的消息类型,每个消息类型都有特定的字段和含义。在导航中,最常见的NMEA 0183消息类型包括:GGA(Global Positioning System Fix Data):包含定位相关的信息,如纬度、经度、定位质量指示、使用卫星数量、水平定位精度因子等。GLL(Geographic Position – Latitude/Longitude):提供纬度、经度和时间信息。GSA(GNSS DOP and Active Satellites):包含定位模式、使用卫星编号和位置精度因子等信息。GSV(GNSS Satellites in View):提供可见卫星的信息,包括卫星编号、仰角、方位角和信噪比等。RMC(Recommended Minimum Specific GNSS Data):包含定位状态、纬度、经度、地面速度、地面航向等。VTG(Course Over Ground and Ground Speed):提供地面航向和速度信息。ZDA(Time and Date):包含UTC时间和日期信息。这些消息类型涵盖了定位、导航和时间相关的数据,可以用于实时定位、航行导航以及时间同步等应用。NMEA 0183格式的数据通常由GPS接收器、导航仪、自动驾驶系统等设备产生,并通过串口输出。其他设备可以通过读取串口数据,并按照NMEA 0183的规范解析数据。这样,不同设备之间就可以进行数据交换和共享,实现设备之间的互操作性。随着技术的发展,新一代的GPS设备也开始采用更高级的数据格式,例如NMEA 2000。然而,由于广泛应用和兼容性的要求,NMEA 0183仍然被广泛支持,并被许多设备和导航系统所使用。下面是支持NMEA 0183格式的GPS模块输出的定位数据:$GNGGA,114955.000,2842.4158,N,11549.5439,E,1,05,3.8,54.8,M,0.0,M,,*4F$GNGLL,2842.4158,N,11549.5439,E,114955.000,A,A*4D$GPGSA,A,3,10,31,18,,,,,,,,,,5.7,3.8,4.2*37$BDGSA,A,3,07,10,,,,,,,,,,,5.7,3.8,4.2*2A$GPGSV,3,1,10,10,49,184,42,12,16,039,,14,54,341,,18,22,165,23*7B$GPGSV,3,2,10,22,11,318,,25,51,055,,26,24,205,,29,13,110,*7C$GPGSV,3,3,10,31,50,287,36,32,66,018,*7F$BDGSV,1,1,04,03,,,07,05,,,29,07,79,246,33,10,52,232,19*62$GNRMC,114955.000,A,2842.4158,N,11549.5439,E,0.00,44.25,061117,,,A*4D$GNVTG,44.25,T,,M,0.00,N,0.00,K,A*14$GNZDA,114955.000,06,11,2017,00,00*47$GPTXT,01,01,01,ANTENNA OK*35二、GPS字段含义这段GPS数据是NMEA 0183格式的数据,它包含了不同类型的GPS消息,每个消息都有特定的含义和字段。(1)$GNGGA,114955.000,2842.4158,N,11549.5439,E,1,05,3.8,54.8,M,0.0,M,,*4F 这是GGA(Global Positioning System Fix Data)消息,包含了以下关键信息:时间:11时49分55秒(UTC时间)纬度:28度42.4158分北纬经度:115度49.5439分东经定位质量指示:1(表示定位有效)使用卫星数量:5颗卫星HDOP(Horizontal Dilution of Precision)水平定位精度因子:3.8海拔高度:54.8米大地水准面高度:0.0米(2)$GNGLL,2842.4158,N,11549.5439,E,114955.000,A,A*4D 这是GLL(Geographic Position – Latitude/Longitude)消息,包含了以下关键信息:纬度:28度42.4158分北纬经度:115度49.5439分东经时间:11时49分55秒(UTC时间)定位状态:A(表示定位有效)导航模式指示:A(自主定位导航)(3)$GPGSA,A,3,10,31,18,,,,,,,,,,5.7,3.8,4.2*37 这是GSA(GNSS DOP and Active Satellites)消息,包含了以下关键信息:定位模式:自主定位模式定位类型:三维定位使用卫星编号:10、31、18PDOP(Position Dilution of Precision)位置精度因子:5.7HDOP(Horizontal Dilution of Precision)水平定位精度因子:3.8VDOP(Vertical Dilution of Precision)垂直定位精度因子:4.2(4)$BDGSA,A,3,07,10,,,,,,,,,,,5.7,3.8,4.2*2A 这是BDGSA(Beidou GNSS DOP and Active Satellites)消息,与GPGSA消息类似,但使用的是北斗导航系统的数据。消息序号:这组消息是一共分为3个消息,这是第1个消息可见卫星总数:10颗卫星卫星编号、仰角、方位角和信噪比等信息(5)$BDGSV,1,1,04,03,,,07,05,,,29,07,79,246,33,10,52,232,19*62 这是BDGSV(Beidou GNSS Satellites in View)消息,与GPGSV消息类似,但使用的是北斗导航系统的数据。(6)$GNRMC,114955.000,A,2842.4158,N,11549.5439,E,0.00,44.25,061117,,,A*4D 这是RMC(Recommended Minimum Specific GNSS Data)消息,包含了以下关键信息:时间:11时49分55秒(UTC时间)定位状态:A(表示定位有效)纬度:28度42.4158分北纬经度:115度49.5439分东经地面速度:0.00节地面航向:44.25度日期:06日11月17年(7)$GNVTG,44.25,T,,M,0.00,N,0.00,K,A*14 这是VTG(Course Over Ground and Ground Speed)消息,包含了以下关键信息:地面航向:44.25度(真北参考)地面速度:0.00节(节与海里/小时是相同的)地面速度:0.00千米/小时模式指示:A(自主定位导航模式)(8)$GNZDA,114955.000,06,11,2017,00,00*47 这是ZDA(Time and Date)消息,包含了以下关键信息:UTC时间:11时49分55秒日期:06日11月17年本地时区偏移:00小时00分钟(9)$GPTXT,01,01,01,ANTENNA OK*35 这是TXT(Text Transmission)消息,包含了以下关键信息:文本内容:ANTENNA OK(表示天线状态良好)这些消息提供了GPS设备的时间、位置、定位质量、可见卫星数量等信息。其中涉及到的字段包括时间(UTC时间)、纬度、经度、定位质量指示、使用卫星编号、定位精度因子、海拔高度、速度等。根据不同的应用需求,可以从这些数据中提取出需要的信息来进行处理和分析。三、C语言解析数据代码3.1 解析每个字段数据以下是使用C语言解析NMEA 0183数据字段并将其打印到串口:#include <stdio.h>#include <string.h>​// 函数声明void parseNMEA(const char* sentence);void printField(const char* field);​int main() { // NMEA数据 const char* data[] = { "$GNGGA,114955.000,2842.4158,N,11549.5439,E,1,05,3.8,54.8,M,0.0,M,,*4F", "$GNGLL,2842.4158,N,11549.5439,E,114955.000,A,A*4D", "$GPGSA,A,3,10,31,18,,,,,,,,,,5.7,3.8,4.2*37", "$BDGSA,A,3,07,10,,,,,,,,,,,5.7,3.8,4.2*2A", "$GPGSV,3,1,10,10,49,184,42,12,16,039,,14,54,341,,18,22,165,23*7B", "$GPGSV,3,2,10,22,11,318,,25,51,055,,26,24,205,,29,13,110,*7C", "$GPGSV,3,3,10,31,50,287,36,32,66,018,*7F", "$BDGSV,1,1,04,03,,,07,05,,,29,07,79,246,33,10,52,232,19*62", "$GNRMC,114955.000,A,2842.4158,N,11549.5439,E,0.00,44.25,061117,,,A*4D", "$GNVTG,44.25,T,,M,0.00,N,0.00,K,A*14", "$GNZDA,114955.000,06,11,2017,00,00*47", "$GPTXT,01,01,01,ANTENNA OK*35" };​ int dataSize = sizeof(data) / sizeof(data[0]);​ // 解析并打印每个数据 for (int i = 0; i < dataSize; i++) { parseNMEA(data[i]); }​ return 0;}​// 解析NMEA语句void parseNMEA(const char* sentence) { const char* field; int count = 0;​ // 跳过$和逗号 field = strchr(sentence, ','); if (field == NULL) { return; } field++;​ // 解析每个字段 while (*field != '*') { printField(field);​ // 查找下一个逗号或结束符 field = strchr(field, ','); if (field == NULL) { break; } field++; count++; }}​// 打印字段数据void printField(const char* field) { char str[50]; int len = 0;​ // 查找字段的长度 while (field[len] != ',' && field[len] != '\0' && field[len] != '*') { len++; }​ // 复制字段数据到缓冲区 strncpy(str, field, len); str[len] = '\0';​ // 打印字段数据到串口 printf("%s\n", str);}3.2 解析定位数据定义了一个名为GPSData的结构体,并将解析后的定位数据存储在该结构体的各个变量中:#include <stdio.h>#include <string.h>​// 定义结构体typedef struct { char time[10]; char latitude[10]; char longitude[11]; int fixStatus; int satellites; float hdop; float altitude;} GPSData;​// 函数声明void parseNMEA(const char* sentence, GPSData* gpsData);void printGPSData(const GPSData* gpsData);​int main() { // NMEA数据 const char* data[] = { "$GNGGA,114955.000,2842.4158,N,11549.5439,E,1,05,3.8,54.8,M,0.0,M,,*4F", "$GNGLL,2842.4158,N,11549.5439,E,114955.000,A,A*4D", "$GPGSA,A,3,10,31,18,,,,,,,,,,5.7,3.8,4.2*37", "$BDGSA,A,3,07,10,,,,,,,,,,,5.7,3.8,4.2*2A", "$GPGSV,3,1,10,10,49,184,42,12,16,039,,14,54,341,,18,22,165,23*7B", "$GPGSV,3,2,10,22,11,318,,25,51,055,,26,24,205,,29,13,110,*7C", "$GPGSV,3,3,10,31,50,287,36,32,66,018,*7F", "$BDGSV,1,1,04,03,,,07,05,,,29,07,79,246,33,10,52,232,19*62", "$GNRMC,114955.000,A,2842.4158,N,11549.5439,E,0.00,44.25,061117,,,A*4D", "$GNVTG,44.25,T,,M,0.00,N,0.00,K,A*14", "$GNZDA,114955.000,06,11,2017,00,00*47", "$GPTXT,01,01,01,ANTENNA OK*35" };​ int dataSize = sizeof(data) / sizeof(data[0]);​ // 解析并打印每个数据 for (int i = 0; i < dataSize; i++) { GPSData gpsData; parseNMEA(data[i], &gpsData); printGPSData(&gpsData); }​ return 0;}​// 解析NMEA语句并存储到结构体中void parseNMEA(const char* sentence, GPSData* gpsData) { const char* field; int count = 0;​ // 跳过$和逗号 field = strchr(sentence, ','); if (field == NULL) { return; } field++;​ // 解析每个字段 while (*field != '*') { switch (count) { case 0: strncpy(gpsData->time, field, 6); gpsData->time[6] = '\0'; break; case 1: strncpy(gpsData->latitude, field, 9); gpsData->latitude[9] = '\0'; break; case 2: strncpy(gpsData->longitude, field, 10); gpsData->longitude[10] = '\0'; break; case 6: sscanf(field, "%d", &gpsData->fixStatus); break; case 7: sscanf(field, "%d", &gpsData->satellites); break; case 8: sscanf(field, "%f", &gpsData->hdop); break; case 9: sscanf(field, "%f", &gpsData->altitude); break; default: break; }​ // 查找下一个逗号或结束符 field = strchr(field, ','); if (field == NULL) { break; } field++; count++; }}​// 打印GPS数据到串口void printGPSData(const GPSData* gpsData) { printf("Time: %s\n", gpsData->time); printf("Latitude: %s\n", gpsData->latitude); printf("Longitude: %s\n", gpsData->longitude); printf("Fix Status: %d\n", gpsData->fixStatus); printf("Satellites: %d\n", gpsData->satellites); printf("HDOP: %.1f\n", gpsData->hdop); printf("Altitude: %.1f meters\n", gpsData->altitude); printf("\n");}这段代码会解析NMEA 0183格式的数据,并将解析的结果存储在GPSData结构体的对应变量中。使用printGPSData函数将数据打印到串口。
  • [技术干货] C语言实例_奇偶校验算法
    一、奇偶校验算法奇偶校验算法(Parity Check Algorithm)是一种简单的错误检测方法,用于验证数据传输中是否发生了位错误。通过在数据中添加一个附加的奇偶位(即校验位),来实现错误的检测和纠正。在奇偶校验算法中,假设每个字节由8个比特(位)组成。奇偶校验位的值取决于数据字节中的1的个数。如果数据字节中1的个数是偶数个,奇偶校验位被设置为0;如果1的个数是奇数个,奇偶校验位被设置为1。这样,在接收端,通过统计接收到的数据字节中1的个数,就可以检测出位错误。具体的奇偶校验算法包括以下几个步骤:(1)发送端:在发送数据字节之前,统计数据字节中1的个数,根据个数设置奇偶校验位的值,并将数据字节和奇偶校验位一起发送。(2)接收端:在接收数据字节后,再次统计接收到的数据字节中1的个数,与接收到的奇偶校验位进行比较。如果两者不一致,说明数据传输中发生了位错误。奇偶校验算法在以下场景中常被使用:(1)串行通信:在串行通信中,奇偶校验算法可以用于检测数据传输过程中发生的位错误。发送端计算奇偶校验位并附加到发送的数据字节上,接收端通过验证奇偶校验位来判断接收到的数据是否正确。(2)存储介质:在一些存储介质上,如磁盘驱动器或闪存存储器,奇偶校验算法可以用于检测数据读取或写入过程中发生的位错误。在存储数据时,计算奇偶校验位并与数据一起存储;在读取数据时,再次计算校验位并与存储的校验位进行比较,以确保数据的完整性和准确性。(3)错误检测:奇偶校验算法也可以用于其他需要简单错误检测的场景。例如,在计算机内存或寄存器中,奇偶校验位可以用于检测存储数据过程中的位错误,以避免数据的错误使用或传输。奇偶校验算法只能检测到位错误,而不能纠正错误。如果检测到错误,则需要采取其他纠错措施或请求重新传输数据。二、代码实现场景:在单片机通信里,单片机需要向上位机发送数据。 下面代码演示两个函数,针对发送方和接收方使用,使用奇偶校验算法对数据进行验证。2.1 发送方函数void sender_send_data_with_parity(unsigned char* data, int length) { // 统计数据字节中1的个数 int count = 0; for (int i = 0; i < length; i++) { unsigned char byte = data[i]; for (int j = 0; j < 8; j++) { if ((byte >> j) & 1) { count++; } } }​ // 计算奇偶校验位,如果1的个数是偶数,则校验位为0,否则为1 unsigned char parity_bit = (count % 2 == 0) ? 0 : 1;​ // 发送数据字节和奇偶校验位 for (int i = 0; i < length; i++) { send_byte(data[i]); } send_byte(parity_bit);}2.2 接收方函数void receiver_receive_data_with_parity() { // 接收数据 unsigned char received_data[MAX_LENGTH]; int length = receive_data(received_data);​ // 统计接收到的数据字节中1的个数 int count = 0; for (int i = 0; i < length - 1; i++) { unsigned char byte = received_data[i]; for (int j = 0; j < 8; j++) { if ((byte >> j) & 1) { count++; } } }​ // 比较接收到的奇偶校验位与数据字节中1的个数是否一致 unsigned char expected_parity_bit = (count % 2 == 0) ? 0 : 1; unsigned char received_parity_bit = received_data[length - 1];​ if (expected_parity_bit != received_parity_bit) { // 发生了位错误 handle_error(); } else { // 数据传输正常 process_data(received_data, length - 1); }}
  • [问题求助] ffmpeg 视频剪切问题
    现在想用ffmpeg剪切视频,如何能精确定位时间呀? 每次剪切总是会有2秒以上的误差。 剪切是靠时间还是靠视频帧呢?
  • [技术干货] C语言实例_CRC校验算法
    一、CRC介绍CRC(Cyclic Redundancy Check,循环冗余校验)是一种常用的错误检测技术,用于验证数据在传输或存储过程中是否发生了错误。它通过对数据进行一系列计算和比较,生成一个校验值,并将其附加到数据中。接收方可以使用相同的算法对接收到的数据进行校验,然后与接收到的校验值进行比较,从而确定数据是否存在错误。CRC校验通常用于以下方面:(1)数据传输的可靠性:在数据通过媒体或网络进行传输时,可能会发生噪声、干扰或其他传输错误。通过在数据中添加CRC校验值,接收方可以检测到传输过程中是否发生了错误,并采取相应措施,如请求重新发送数据。(2)存储介质的完整性检测:在存储介质上读取或写入数据时,可能会发生位翻转、介质故障等错误。通过在数据存储时使用CRC校验,可以在读取数据时检测到这些错误,并提供数据的完整性保证。(3)网络通信协议:许多网络通信协议(如Ethernet、WiFi、USB等)使用CRC校验作为数据帧的一部分,以确保传输的数据准确无误。接收方在接收到数据帧后,使用CRC校验来验证数据的完整性。在项目中,CRC校验广泛应用于各种通信系统、存储系统和数据传输系统中。通过使用CRC校验,可以提高数据的可靠性,并减少传输或存储过程中的错误。它可以检测到数据位级别的错误,并提供一定程度的数据完整性保证。CRC校验在保障数据可靠性和完整性方面具有重要作用,特别是在对数据完整性有较高要求的应用场景中。二、示例代码以下C语言代码演示如何获取一段数据的CRC校验值:#include <stdio.h>#include <stdint.h>​// CRC校验函数uint16_t crc16(uint8_t *data, int length){ uint16_t crc = 0xFFFF; for (int i = 0; i < length; i++) { crc ^= data[i]; for (int j = 0; j < 8; j++) { if (crc & 1) { crc >>= 1; crc ^= 0xA001; } else { crc >>= 1; } } } return crc;}​// 封装的CRC校验函数调用uint16_t calculateCRC(uint8_t *data, int length){ return crc16(data, length);}​int main(){ uint8_t message[] = {0x01, 0x02, 0x03, 0x04, 0x05}; int length = sizeof(message) / sizeof(message[0]); uint16_t crc = calculateCRC(message, length); printf("CRC: 0x%04X\n", crc); return 0;}在上面代码中,crc16 函数实现了CRC校验的计算逻辑。采用了常用的CRC-16算法(0xA001多项式)。calculateCRC 函数是对 crc16 的封装,用于调用CRC校验函数并返回校验结果。在 main 函数中,通过调用 calculateCRC 函数来计算给定数据的CRC校验值,并将结果打印输出。代码中的CRC校验函数和封装函数是基于无符号8位字节和无符号16位整数的数据类型进行计算的。三、案例:数据校验场景:在单片机通信里,单片机需要向上位机发送一段数据。比如,存放在char buff[1024];这个数组里。 需要封装两个函数,单片机端调用函数对这段数据进行CRC校验,封装校验值,然后上位机收到数据之后验证CRC,校验数据是否传输正确。3.1 发送方(封装校验值)#include <stdio.h>#include <stdint.h>​// CRC校验函数uint16_t crc16(uint8_t *data, int length){ uint16_t crc = 0xFFFF; for (int i = 0; i < length; i++) { crc ^= data[i]; for (int j = 0; j < 8; j++) { if (crc & 1) { crc >>= 1; crc ^= 0xA001; } else { crc >>= 1; } } } return crc;}​// 封装CRC校验值到数据中void appendCRC(uint8_t *data, int length){ uint16_t crc = crc16(data, length); data[length] = crc & 0xFF; // 将低8位放入数据末尾 data[length + 1] = crc >> 8; // 将高8位放入数据末尾的下一个位置}​int main(){ uint8_t buff[1024] = {0x01, 0x02, 0x03, 0x04, 0x05}; // 原始数据 int length = 5; // 数据长度 // 在原始数据后追加CRC校验值 appendCRC(buff, length); // 输出发送的数据(包括CRC校验值) printf("发送的数据:"); for (int i = 0; i < length + 2; i++) { printf("%02X ", buff[i]); } printf("\n"); return 0;}在发送方的代码中,使用 appendCRC 函数将CRC校验值追加到原始数据的末尾。3.2 接收方(校验数据)#include <stdio.h>#include <stdint.h>​// CRC校验函数uint16_t crc16(uint8_t *data, int length){ uint16_t crc = 0xFFFF; for (int i = 0; i < length; i++) { crc ^= data[i]; for (int j = 0; j < 8; j++) { if (crc & 1) { crc >>= 1; crc ^= 0xA001; } else { crc >>= 1; } } } return crc;}​// 验证CRC校验值是否正确int verifyCRC(uint8_t *data, int length){ uint16_t crc = crc16(data, length - 2); // 去除数据末尾的CRC校验值 // 获取接收到的CRC校验值 uint16_t receivedCRC = (data[length - 1] << 8) | data[length - 2]; // 比较计算得到的CRC校验值与接收到的CRC校验值 if (crc == receivedCRC) { return 1; // 校验通过 } else { return 0; // 校验失败 }}​int main(){ uint8_t receivedData[] = {0x01, 0x02, 0x03, 0x04, 0x05, 0xC2, 0x45}; // 收到的数据(包括CRC校验值) int length = sizeof(receivedData) / sizeof(receivedData[0]); // 验证CRC校验值是否正确 int crcResult = verifyCRC(receivedData, length); if (crcResult) { printf("CRC校验通过\n"); // TODO: 进一步处理正确的数据 } else { printf("CRC校验失败\n"); // TODO: 处理校验失败的情况 } return 0;}在接收方的代码中,使用 verifyCRC 函数验证接收到的数据的CRC校验值是否正确。如果校验通过,可以执行进一步的数据处理操作;如果校验失败,可以进行异常处理。示例中的CRC校验函数是基于无符号8位字节和无符号16位整数的数据类型进行计算的。可以根据实际需求进行适当修改,以适应不同的数据类型和CRC算法。
  • [技术干货] Linux下C语言调用libcurl库获取天气预报信息
    一、概述当前文章介绍如何在Linux(Ubuntu)下使用C语言调用libcurl库获取天气预报的方法。通过HTTP GET请求访问百度天气API,并解析返回的JSON数据,可以获取指定城市未来7天的天气预报信息。二、设计思路【1】使用libcurl库进行HTTP GET请求在代码中包含<curl/curl.h>头文件,以便使用libcurl库使用curl_easy_init()函数初始化curl设置请求选项,包括URL、写回调函数和写数据参数使用curl_easy_perform()函数执行请求【2】编写回调函数,将响应数据存储在内存中定义一个结构体,包含存储响应数据的指针和长度在回调函数中将响应数据拷贝到内存中,并动态调整内存大小返回已拷贝的数据大小【3】解析JSON数据使用json_tokener_parse()函数解析返回的JSON数据使用json_object_object_get_ex()函数获取指定字段的值使用json_object_array_length()函数获取数组长度使用json_object_array_get_idx()函数获取数组中的元素使用json_object_get_string()函数获取字符串值【4】打印天气预报信息遍历获取到的天气预报数据,依次获取日期、天气和温度使用printf()函数打印每一天的天气预报信息三、关键代码以下是主要的代码片段:// 定义回调函数,用于将响应数据存储在内存中size_t write_callback(void *ptr, size_t size, size_t nmemb, void *stream) { // ...}​// 子函数,用于获取指定城市未来7天的天气预报int get_weather_forecast(const char *city) { // ...}​int main() { const char *city = "your_city_code"; int ret = get_weather_forecast(city); // ...}四、使用说明【1】替换API密钥和城市代码:在示例代码中,将your_ak和your_city_code替换为你自己的百度API密钥和城市代码。【2】编译代码:使用合适的C编译器,如gcc,编译代码。gcc -o download_program download_program.c -lcurl【3】运行代码:在终端中运行生成的可执行文件。./download_program【4】查看天气预报:程序会打印出指定城市未来7天的天气预报信息。五、完整代码HTTP GET请求访问百度天气API,并解析返回的JSON数据获取需要的天气信息。#include <stdio.h>#include <stdlib.h>#include <string.h>#include <curl/curl.h>#include <json-c/json.h>​// 定义回调函数,用于将响应数据存储在内存中size_t write_callback(void *ptr, size_t size, size_t nmemb, void *stream) { size_t realsize = size * nmemb; struct string *mem = (struct string *)stream;​ mem->ptr = realloc(mem->ptr, mem->len + realsize + 1); if (mem->ptr == NULL) { fprintf(stderr, "内存分配失败\n"); return 0; }​ memcpy(&(mem->ptr[mem->len]), ptr, realsize); mem->len += realsize; mem->ptr[mem->len] = '\0';​ return realsize;}​// 子函数,用于获取指定城市未来7天的天气预报int get_weather_forecast(const char *city) { char url[256]; sprintf(url, "https://api.map.baidu.com/weather/v1/?district_id=%s&ak=your_ak", city);​ CURL *curl = curl_easy_init(); struct string response; response.ptr = malloc(1); response.len = 0;​ if (curl && response.ptr) { // 设置请求选项 curl_easy_setopt(curl, CURLOPT_URL, url); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback); curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);​ // 执行请求 CURLcode res = curl_easy_perform(curl); if (res != CURLE_OK) { fprintf(stderr, "请求失败: %s\n", curl_easy_strerror(res)); free(response.ptr); curl_easy_cleanup(curl); return -1; }​ // 解析JSON数据 struct json_object *json = json_tokener_parse(response.ptr); if (json == NULL) { fprintf(stderr, "JSON解析失败\n"); free(response.ptr); curl_easy_cleanup(curl); return -1; }​ // 解析天气预报 struct json_object *result, *weather_data; json_object_object_get_ex(json, "result", &result); json_object_object_get_ex(result, "weather_data", &weather_data);​ int i; int num_days = json_object_array_length(weather_data); for (i = 0; i < num_days; i++) { struct json_object *day = json_object_array_get_idx(weather_data, i); const char *date, *weather, *temperature; date = json_object_get_string(json_object_object_get(day, "date")); weather = json_object_get_string(json_object_object_get(day, "weather")); temperature = json_object_get_string(json_object_object_get(day, "temperature"));​ printf("日期:%s\n天气:%s\n温度:%s\n\n", date, weather, temperature); }​ free(response.ptr); json_object_put(json); } else { fprintf(stderr, "初始化失败\n"); if (response.ptr) { free(response.ptr); } if (curl) { curl_easy_cleanup(curl); } return -1; }​ curl_easy_cleanup(curl); return 0;}​int main() { const char *city = "your_city_code"; int ret = get_weather_forecast(city); if (ret == 0) { printf("天气预报获取成功!\n"); } else { printf("天气预报获取失败!\n"); }​ return 0;}在示例代码中,使用curl_easy_setopt函数设置HTTP GET请求的URL,并通过CURLOPT_WRITEFUNCTION和CURLOPT_WRITEDATA选项指定回调函数,将响应数据存储在内存中。然后,使用json_tokener_parse函数解析返回的JSON数据,并提取其中的天气预报信息。通过json_object_object_get和json_object_array_get_idx等函数获取JSON对象和数组中的数据。注意:代码中的URL中的YOUR_AK和YOUR_CITY_CODE需要使用你自己的百度API密钥和城市代码替换。通过调用get_weather_forecast函数,可以获取指定城市未来7天的天气预报并打印出来。
  • [技术干货] Linux下C语言调用libcurl库下载文件到本地
    一、项目介绍当前文章介绍如何使用C语言调用libcurl库在Linux(Ubuntu)操作系统下实现网络文件下载功能。libcurl是一个开源的跨平台网络传输库,用于在C和C++等编程语言中实现各种网络通信协议的客户端功能。它支持多种协议,包括HTTP、HTTPS、FTP、SMTP、POP3等,可以方便地进行数据的上传和下载操作。以下是libcurl库的一些主要特点和功能:1. 跨平台性:libcurl库可以在多个操作系统上使用,包括Windows、Linux、macOS等。这使得开发者可以轻松地编写跨平台的网络应用程序。2. 多协议支持:libcurl支持多种网络协议,包括HTTP、HTTPS、FTP、SMTP、POP3等。它提供了丰富的API,使得开发者可以通过简单的接口调用来实现与远程服务器之间的通信。3. 断点续传:libcurl支持断点续传功能,即可以从已经下载的位置继续下载文件。这对于大文件的下载非常有用,可以节省带宽和时间,并避免重新下载整个文件。4. SSL/TLS支持:libcurl可以通过OpenSSL或其他TLS/SSL库来进行安全传输。它支持HTTPS协议,并提供了SSL证书验证、加密和解密等功能,以确保数据的安全性。5. 异步和多线程支持:libcurl提供了异步和多线程操作的支持,可以在网络传输过程中进行其他任务处理,提高程序的并发性和性能。6. 适应性和灵活性:libcurl库提供了丰富的选项和回调函数,允许开发者根据自己的需求进行定制和扩展。开发者可以配置代理服务器、设置超时时间、自定义HTTP头部等。7. 良好的错误处理和调试支持:libcurl提供了详细的错误代码和错误信息,方便开发者进行错误处理和故障排除。它还提供了调试输出功能,可打印详细的网络通信和传输信息。8. 并发连接管理:libcurl支持并发连接管理,可以同时处理多个网络请求。这对于高并发的网络应用非常有用,可以提高系统的吞吐量和性能。二、环境准备libcurl库: 可以通过在终端中运行以下命令进行安装:sudo apt-get install libcurl4-openssl-devGitHub仓库:cid:link_0 libcurl官网: cid:link_1三、设计步骤3.1 引入头文件在C代码文件中,需要引入curl/curl.h头文件,以便使用libcurl库提供的函数和结构体。#include <stdio.h>#include <curl/curl.h>3.2 初始化libcurl在程序开始之前,需要初始化libcurl库。可以通过调用curl_global_init函数来完成。curl_global_init(CURL_GLOBAL_DEFAULT);3.3 设置下载选项接下来,需要设置下载选项,包括要下载的URL链接、保存到本地的文件路径等。可以使用curl_easy_setopt函数来设置选项。CURL *curl = curl_easy_init();if (curl) { curl_easy_setopt(curl, CURLOPT_URL, "http://example.com/file.zip"); curl_easy_setopt(curl, CURLOPT_WRITEDATA, fp); // fp是文件指针,用于保存下载的数据}3.4 执行下载请求调用curl_easy_perform函数来执行下载请求,并将文件保存到指定路径。在执行过程中,libcurl库会自动处理网络传输和接收文件数据。CURLcode res = curl_easy_perform(curl);if (res != CURLE_OK) { fprintf(stderr, "下载失败: %s\n", curl_easy_strerror(res));}3.5 清理资源最后,在程序结束时,需要清理libcurl的资源。可以通过调用curl_easy_cleanup函数来完成。curl_easy_cleanup(curl);3.6 完整示例代码下面是一个完整的示例代码,演示如何使用C语言和libcurl库在Linux(Ubuntu)下实现网络文件下载功能:#include <stdio.h>#include <curl/curl.h>​int main() { CURL *curl = curl_easy_init(); FILE *fp = fopen("downloaded_file.zip", "wb"); //打开一个文件用于保存下载的数据​ if (curl && fp) { curl_easy_setopt(curl, CURLOPT_URL, "http://example.com/file.zip"); curl_easy_setopt(curl, CURLOPT_WRITEDATA, fp);​ CURLcode res = curl_easy_perform(curl); if (res != CURLE_OK) { fprintf(stderr, "下载失败: %s\n", curl_easy_strerror(res)); }​ fclose(fp); } else { fprintf(stderr, "初始化失败\n"); }​ curl_easy_cleanup(curl); curl_global_cleanup();​ return 0;}3.7 编译和运行在终端中,使用以下命令编译示例代码:gcc -o download_program download_program.c -lcurl然后,通过运行生成的可执行文件来执行下载程序:./download_program四、完整代码下面是一个封装了网络文件下载功能的子函数:#include <stdio.h>#include <curl/curl.h>// 定义回调函数,用于将下载的数据写入本地文件size_t write_callback(void *ptr, size_t size, size_t nmemb, void *stream) { return fwrite(ptr, size, nmemb, (FILE *)stream);}// 子函数,用于下载网络文件到本地int download_file(const char *url, const char *output_filename) { CURL *curl = curl_easy_init(); FILE *fp = fopen(output_filename, "wb"); // 打开一个文件用于保存下载的数据 if (curl && fp) { // 设置下载选项 curl_easy_setopt(curl, CURLOPT_URL, url); curl_easy_setopt(curl, CURLOPT_WRITEDATA, fp); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback); // 执行下载请求 CURLcode res = curl_easy_perform(curl); if (res != CURLE_OK) { fprintf(stderr, "下载失败: %s\n", curl_easy_strerror(res)); fclose(fp); curl_easy_cleanup(curl); return -1; } fclose(fp); } else { fprintf(stderr, "初始化失败\n"); if (fp) { fclose(fp); } if (curl) { curl_easy_cleanup(curl); } return -1; } curl_easy_cleanup(curl); return 0;}int main() { const char *url = "http://example.com/file.zip"; const char *output_filename = "downloaded_file.zip"; int ret = download_file(url, output_filename); if (ret == 0) { printf("文件下载成功!\n"); } else { printf("文件下载失败!\n"); } return 0;}在上面的代码中,download_file函数实现了下载网络文件到本地的功能。将要下载的URL链接和保存到本地的文件路径作为函数参数传入。函数内部使用libcurl库设置下载选项,执行下载请求,并将数据写入本地文件。在main函数中,可以调用download_file函数来实现文件下载。通过判断函数返回值,可以判断文件下载是否成功。编译和运行代码的步骤与之前提供的步骤相同。通过调用download_file函数实现网络文件下载功能,可以方便地在其他代码中复用该功能,并进行错误处理和扩展。
  • [技术干货] 基于STM32设计的出租车计费系统
    一、项目介绍在城市交通中,出租车是一种常见的交通工具。为了方便乘客和司机之间的交易,出租车计费系统被广泛应用于出租车行业。系统能够自动计算乘客的费用,提供准确、方便的计费服务,并且能够记录乘客的行驶数据,方便后续查询和管理。传统的出租车计费方式是基于人工计算,司机根据里程和时间进行估算并告知乘客费用。然而,这种计费方式容易存在误差和争议,并且对司机和乘客都不够方便和透明。因此,出租车行业迫切需要一种更加准确、高效和可靠的计费系统。基于此背景,本项目设计和开发一种基于STM32微控制器的出租车计费系统,以替代传统的人工计费方式。该系统将利用STM32微控制器的强大处理能力和丰富的外设接口,集成各种功能模块,实现自动计算乘客费用、显示计费信息等功能。通过该出租车计费系统,乘客只需在上车时按下对应按钮,系统将自动开始计费,并在显示屏上实时显示行驶时间、里程和费用等信息。乘客还可以通过按键输入特殊情况,如堵车或夜间行驶,以便系统进行相应的额外计费。当乘客下车时,系统将自动停止计费,并显示最终费用。同时,系统还将记录乘客的行驶数据以备查询和管理。二、系统设计思路2.1 系统架构出租车计费系统的主要组成部分包括:STM32微控制器、LCD显示屏、按键、计时电路、收费器和外部存储器。整个系统的架构如下:STM32微控制器:采用STM32F103RCT6作为系统的控制核心,负责接收并处理来自各个模块的输入信号,并控制液晶显示屏上的信息显示和收费器的操作。LCD显示屏:采用1.44寸LCD显示屏,用于显示当前的计费信息,包括行驶时间、里程和费用等。按键:用于输入乘客上车和下车的时间以及其他特殊情况,如堵车、夜间行驶等。计时电路:用于准确地测量行驶时间。收费器:负责根据计费规则和实时数据计算乘客的费用。外部存储器:用于存储行驶数据和计费规则。2.2 系统功能出租车计费系统具有以下主要功能:实时计算行驶时间和里程。根据计费规则自动计算乘客费用。在LCD显示屏上显示当前的计费信息。支持特殊情况的额外计费,如堵车、夜间行驶等。存储行驶数据和计费规则以备查询和更新。三、代码设计3.1 LCD显示屏代码#include "stm32f10x.h"​// 定义LCD引脚连接#define LCD_RS_PIN GPIO_Pin_0#define LCD_RS_PORT GPIOA#define LCD_RW_PIN GPIO_Pin_1#define LCD_RW_PORT GPIOA#define LCD_E_PIN GPIO_Pin_2#define LCD_E_PORT GPIOA#define LCD_D4_PIN GPIO_Pin_3#define LCD_D4_PORT GPIOA#define LCD_D5_PIN GPIO_Pin_4#define LCD_D5_PORT GPIOA#define LCD_D6_PIN GPIO_Pin_5#define LCD_D6_PORT GPIOA#define LCD_D7_PIN GPIO_Pin_6#define LCD_D7_PORT GPIOA​// 定义命令和数据的宏#define LCD_COMMAND 0#define LCD_DATA 1​// 延时函数,用于产生适当的延时void Delay(uint32_t nCount) { for (; nCount != 0; --nCount) { }}​// 发送命令或数据到LCD函数void LCD_Send(uint8_t byte, uint8_t mode) { GPIO_WriteBit(LCD_RS_PORT, LCD_RS_PIN, (mode == LCD_DATA) ? Bit_SET : Bit_RESET); GPIO_WriteBit(LCD_RW_PORT, LCD_RW_PIN, Bit_RESET); GPIO_WriteBit(LCD_D4_PORT, LCD_D4_PIN, (byte >> 4) & 0x01 ? Bit_SET : Bit_RESET); GPIO_WriteBit(LCD_D5_PORT, LCD_D5_PIN, (byte >> 5) & 0x01 ? Bit_SET : Bit_RESET); GPIO_WriteBit(LCD_D6_PORT, LCD_D6_PIN, (byte >> 6) & 0x01 ? Bit_SET : Bit_RESET); GPIO_WriteBit(LCD_D7_PORT, LCD_D7_PIN, (byte >> 7) & 0x01 ? Bit_SET : Bit_RESET); GPIO_WriteBit(LCD_E_PORT, LCD_E_PIN, Bit_SET); Delay(1000); GPIO_WriteBit(LCD_E_PORT, LCD_E_PIN, Bit_RESET); GPIO_WriteBit(LCD_D4_PORT, LCD_D4_PIN, (byte >> 0) & 0x01 ? Bit_SET : Bit_RESET); GPIO_WriteBit(LCD_D5_PORT, LCD_D5_PIN, (byte >> 1) & 0x01 ? Bit_SET : Bit_RESET); GPIO_WriteBit(LCD_D6_PORT, LCD_D6_PIN, (byte >> 2) & 0x01 ? Bit_SET : Bit_RESET); GPIO_WriteBit(LCD_D7_PORT, LCD_D7_PIN, (byte >> 3) & 0x01 ? Bit_SET : Bit_RESET); GPIO_WriteBit(LCD_E_PORT, LCD_E_PIN, Bit_SET); Delay(1000); GPIO_WriteBit(LCD_E_PORT, LCD_E_PIN, Bit_RESET); Delay(1000);}​// 初始化LCD函数void LCD_Init(void) { Delay(45000); LCD_Send(0x30, LCD_COMMAND); Delay(4500); LCD_Send(0x30, LCD_COMMAND); Delay(150); LCD_Send(0x30, LCD_COMMAND); Delay(150); LCD_Send(0x20, LCD_COMMAND); Delay(150); LCD_Send(0x28, LCD_COMMAND); Delay(150);​ LCD_Send(0x08, LCD_COMMAND); Delay(150); LCD_Send(0x01, LCD_COMMAND); Delay(150); LCD_Send(0x06, LCD_COMMAND); Delay(150); LCD_Send(0x0C, LCD_COMMAND); Delay(150);}​// 在指定位置显示数字函数void LCD_DisplayNumber(uint8_t number, uint8_t x, uint8_t y) { uint8_t data = 0x30 + number; // 转换数字为对应的ASCII码 if (x >= 0 && x < 16 && y >= 0 && y < 2) { uint8_t addr = 0x80 + (y * 0x40) + x; LCD_Send(addr, LCD_COMMAND); LCD_Send(data, LCD_DATA); }}​int main(void) { // 初始化GPIO和LCD RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin = LCD_RS_PIN | LCD_RW_PIN | LCD_E_PIN | LCD_D4_PIN | LCD_D5_PIN | LCD_D6_PIN | LCD_D7_PIN; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); LCD_Init(); while (1) { // 在第一行第一列显示数字1 LCD_DisplayNumber(1, 0, 0); }}​3.2 计时代码通过定时器2实现了收费计时功能,并在串口上打印出计时的实时时间。通过按下'S'键启动计时器,按下'Q'键停止计时器。每隔500毫秒,在串口上打印出实时时间。#include "stm32f10x.h"#include <stdio.h>​// 定义计时状态typedef enum { TIMER_STOPPED, TIMER_RUNNING} TimerState;​TimerState timerState = TIMER_STOPPED; // 计时器初始状态为停止uint32_t startTime = 0; // 开始计时的时间​// 初始化定时器2void Timer2_Init(void) { RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);​ TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_TimeBaseStructure.TIM_Prescaler = 7200 - 1; // 设置预分频值,产生1ms的时间基准 TIM_TimeBaseStructure.TIM_Period = 1000 - 1; // 设置计数器的重载值,每1秒中断一次 TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);​ TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE); TIM_Cmd(TIM2, ENABLE);​ NVIC_InitTypeDef NVIC_InitStructure; NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure);}​// 开始计时void StartTimer(void) { if (timerState == TIMER_STOPPED) { startTime = TIM_GetCounter(TIM2); // 记录开始计时的时间 timerState = TIMER_RUNNING; }}​// 停止计时void StopTimer(void) { if (timerState == TIMER_RUNNING) { timerState = TIMER_STOPPED; }}​// 获取实时时间,返回单位为毫秒uint32_t GetElapsedTime(void) { if (timerState == TIMER_RUNNING) { return TIM_GetCounter(TIM2) - startTime; } else { return 0; }}​// 初始化串口1void USART1_Init(void) { RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE);​ GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; // TX引脚 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure);​ 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_Tx; USART_Init(USART1, &USART_InitStructure);​ USART_Cmd(USART1, ENABLE);}​// 重定向printf函数到串口输出int fputc(int ch, FILE *f) { if (ch == '\n') { USART_SendData(USART1, '\r'); while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET); }​ USART_SendData(USART1, ch); while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);​ return ch;}​int main(void) { // 初始化定时器和串口 Timer2_Init(); USART1_Init();​ printf("Press 'S' to start the timer.\r\n"); printf("Press 'Q' to stop the timer.\r\n");​ while (1) { if (USART_GetFlagStatus(USART1, USART_FLAG_RXNE) != RESET) { uint8_t rxData = (uint8_t)USART_ReceiveData(USART1); if (rxData == 'S' || rxData == 's') { StartTimer(); printf("Timer started.\r\n"); } else if (rxData == 'Q' || rxData == 'q') { StopTimer(); printf("Timer stopped.\r\n"); } }​ // 每隔500毫秒打印实时时间 if (GetElapsedTime() >= 500) { printf("Elapsed time: %lu ms\r\n", GetElapsedTime()); startTime = TIM_GetCounter(TIM2); // 更新开始计时的时间 } }}​// 定时器2中断处理函数void TIM2_IRQHandler(void) { if (TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET) { TIM_ClearITPendingBit(TIM2, TIM_IT_Update); }}​
  • [技术干货] 通过C语言设计的推箱子(控制台终端)
    一、项目介绍推箱子游戏是一款经典的益智小游戏,玩家需要控制主角角色将几个木箱按照要求推到指定位置。在控制台终端中,可以使用字符来表示不同的游戏元素,例如 '#' 表示墙壁, ' ' 表示空地, '$' 表示木箱, '@' 表示主角角色, '+' 表示完成任务的目标位置。实现步骤如下:定义常量和全局变量:需要定义一些常量和全局变量,用于存储游戏中的数据信息,游戏界面的宽度和高度、不同状态下的符号表示、木箱和目标位置等参数。同时还需要定义一个二维字符数组board,用于表示整个游戏界面。初始化游戏界面:在InitGame()函数中进行游戏初始化,设置游戏界面的边框和各个元素的位置。其中,可以使用循环遍历二维字符数组来设置元素的位置,将 '#' 设置为墙壁, ' ' 设置为空地,'$' 设置为木箱等。绘制游戏画面:DrawGame()函数用于绘制游戏画面,并将board数组中的字符逐行输出。可以使用循环遍历二维字符数组来进行输出。更新游戏状态:UpdateGame()函数用于更新游戏状态,包括判断主角角色是否可以移动,以及是否完成任务等操作。可以使用if语句和switch语句来判断不同情况下的操作,判断主角角色是否碰到墙壁或木箱,是否完成任务等。控制主角角色移动:Control()函数用于控制主角角色的移动,读取键盘输入并更新主角角色的位置。可以使用getch()函数获取键盘输入,并根据用户输入进行判断,按下方向键上时主角角色向上移动。判断游戏是否结束:CheckGameOver()函数用于检查游戏是否结束,包括是否成功完成任务或者失败结束游戏。如果判断出游戏结束,则直接退出程序。游戏暂停:Pause()函数用于控制游戏的暂停时间,可以通过调用Sleep()函数来实现。游戏结束:GameOver()函数用于输出最终的游戏得分和游戏结束信息,并直接退出程序。二、代码实现 #include <stdio.h> #include <conio.h> #include <windows.h> ​ //定义常量和全局变量 const int WIDTH = 11; const int HEIGHT = 10; const char WALL = '#'; const char EMPTY = ' '; const char BOX = '$'; const char TARGET = '+'; const char PLAYER = '@'; ​ int playerX, playerY, score; char board[HEIGHT][WIDTH]; ​ //初始化游戏界面 void InitGame() { //设置游戏界面的边框和各个元素的位置 for (int i = 0; i < HEIGHT; i++) { for (int j = 0; j < WIDTH; j++) { if (i == 0 || i == HEIGHT - 1 || j == 0 || j == WIDTH - 1) { board[i][j] = WALL; } else { board[i][j] = EMPTY; } } } ​ //设置木箱和目标位置 board[2][2] = BOX; board[4][5] = BOX; board[6][8] = BOX; board[2][8] = TARGET; board[4][2] = TARGET; board[6][5] = TARGET; ​ //设置主角角色位置 playerX = 7; playerY = 5; board[playerX][playerY] = PLAYER; } ​ //绘制游戏画面 void DrawGame() { system("cls"); for (int i = 0; i < HEIGHT; i++) { for (int j = 0; j < WIDTH; j++) { printf("%c", board[i][j]); } printf("\n"); } printf("Score: %d\n", score); } ​ //更新游戏状态 void UpdateGame(int dx, int dy) { int nextX = playerX + dx; int nextY = playerY + dy; ​ //判断主角角色是否可以移动 if (board[nextX][nextY] == EMPTY || board[nextX][nextY] == TARGET) { board[playerX][playerY] = EMPTY; playerX = nextX; playerY = nextY; board[playerX][playerY] = PLAYER; } else if (board[nextX][nextY] == BOX && (board[nextX + dx][nextY + dy] == EMPTY || board[nextX + dx][nextY + dy] == TARGET)) { //判断主角角色是否可以推动木箱 board[playerX][playerY] = EMPTY; playerX = nextX; playerY = nextY; board[playerX][playerY] = PLAYER; board[nextX + dx][nextY + dy] = BOX; board[nextX][nextY] = EMPTY; } ​ //判断是否完成任务 if (board[2][8] == BOX && board[4][2] == BOX && board[6][5] == BOX) { score += 100; printf("Congratulations! You win!\n"); Sleep(2000); exit(0); } } ​ //控制主角角色移动 void Control() { char c = getch(); switch (c) { case 'w': UpdateGame(-1, 0); break; case 's': UpdateGame(1, 0); break; case 'a': UpdateGame(0, -1); break; case 'd': UpdateGame(0, 1); break; default: break; } } ​ //判断游戏是否结束 void CheckGameOver() { if (board[playerX - 1][playerY] == WALL || board[playerX + 1][playerY] == WALL || board[playerX][playerY - 1] == WALL || board[playerX][playerY + 1] == WALL) { printf("Game over! You lose!\n"); Sleep(2000); exit(0); } } ​ //游戏暂停 void Pause() { Sleep(100); } ​ //游戏结束 void GameOver() { printf("Your final score is: %d\n", score); exit(0); } ​ int main() { //初始化游戏界面 InitGame(); ​ //游戏循环 while (1) { DrawGame(); Control(); CheckGameOver(); Pause(); } ​ return 0; } ​
  • [问题求助] STM32F407VGTx的SPI作为从机调用 hal库API 数据传输问题
    MCU:STM32F407VGTx工具:STM32CubeMX问题:MCU的spi1(从) 与 SOC spi(主) 数据进行传输;调用库函数:HAL_StatusTypeDef HAL_SPI_TransmitReceive(SPI_HandleTypeDef *hspi, uint8_t *pTxData, uint8_t *pRxData, uint16_t Size, uint32_t Timeout)接收和发送没有问题,接收发送数据都OK但是调用HAL_StatusTypeDef HAL_SPI_TransmitReceive_DMA(SPI_HandleTypeDef *hspi, uint8_t *pTxData, uint8_t *pRxData, uint16_t Size)HAL_StatusTypeDef HAL_SPI_TransmitReceive_IT(SPI_HandleTypeDef *hspi, uint8_t *pTxData, uint8_t *pRxData, uint16_t Size)MISO和MOSI的数据都会出现传输错误;代码:循环调用HAL_SPI_TransmitReceive
  • [技术干货] 通过C语言设计的贪吃蛇游戏(控制台终端)
    一、项目介绍当前通过控制台终端实现一个贪吃蛇小游戏,实现游戏的绘制、更新、控制等功能。二、实现效果三、完整代码下面贴出的代码在Windows系统上编译运行,需要使用conio.h头文件中的getch()函数来获取键盘输入,用于控制蛇的移动。可以通过按下'w'、's'、'a'、'd'四个键来分别控制蛇的向上、向下、向左、向右移动。同时还有一个暂停函数Pause(),用于控制蛇的移动速度,可以根据需要调整暂停时间。设计思路:程序定义了一些常量,包括游戏界面的宽度和高度、不同状态下的符号表示等,并在全局变量中定义了蛇头、蛇身、食物等的位置、得分、长度、行进方向等参数。同时还定义了一个二维字符数组board,用于表示整个游戏界面。在InitGame()函数中进行游戏初始化,设置游戏界面的边框、蛇头、蛇身、随机生成食物等操作。DrawGame()函数用于绘制游戏画面,将board数组中的字符逐行输出,并在末尾输出得分。UpdateGame()函数用于更新游戏状态,包括蛇的移动、游戏结束判断等。CheckGameOver()函数用于检查游戏是否结束,包括墙壁碰撞、蛇身碰撞、得分达到上限等情况。GenerateFood()函数用于随机生成食物位置,调用rand()函数获取随机数并进行判断。Move()函数用于蛇的移动,根据当前行进方向进行移动。Control()函数用于用户操作,读取键盘输入并更新蛇的行进方向。Pause()函数用于控制游戏速度,通过调用Sleep()函数控制暂停时间。GameOver()函数用于输出最终得分和游戏结束信息,并直接退出程序。 #include <stdio.h> #include <stdlib.h> #include <conio.h> //需要使用getch()函数 #include <time.h> //需要使用time()函数 #include <Windows.h> ​ ​ #define WIDTH 40 //游戏界面宽度 #define HEIGHT 20 //游戏界面高度 ​ //定义符号常量,表示各种不同的状态 #define BLANK ' ' //空白 #define WALL '*' //墙壁 #define SNAKEHEAD '@' //蛇头 #define SNAKEBODY 'o' //蛇身 #define FOOD '$' //食物 ​ //定义坐标结构体 struct Position { int x; //横坐标 int y; //纵坐标 }; ​ //定义枚举类型,表示游戏状态 enum GameState { Over = -1, //游戏结束 Running = 0, //游戏进行中 Win = 1 //游戏胜利 }; ​ int score = 0; //得分 struct Position head; //蛇头位置 struct Position body[WIDTH * HEIGHT]; //蛇身位置 struct Position food; //食物位置 char board[WIDTH][HEIGHT]; //游戏界面 int length = 3; //蛇身长度,初始为3 int direction = 0; //蛇的行进方向,0表示向右,1表示向下,2表示向左,3表示向上 ​ //函数声明 void InitGame(); //初始化游戏界面和蛇的初始位置 void DrawGame(); //绘制游戏画面 void UpdateGame(); //更新游戏状态 enum GameState CheckGameOver(); //检查游戏是否结束 void GenerateFood(); //生成随机食物 void Move(); //蛇的移动 void Control(); //用户操作,控制蛇的移动 void Pause(); //游戏暂停 void GameOver(); //游戏结束 ​ int main() { srand(time(NULL)); //用当前时间作为随机数种子,使每次运行的随机食物位置不同 InitGame(); //初始化游戏 DrawGame(); //绘制游戏画面 while (1) { UpdateGame(); //更新游戏 DrawGame(); //绘制游戏画面 Control(); //用户操作,控制蛇的移动 Pause(); //游戏暂停 } return 0; } ​ //初始化游戏界面和蛇的初始位置 void InitGame() { for (int i = 0; i < WIDTH; i++) { for (int j = 0; j < HEIGHT; j++) { if (i == 0 || j == 0 || i == WIDTH - 1 || j == HEIGHT - 1) //设置墙壁 board[i][j] = WALL; else board[i][j] = BLANK; //其他为空白 } } ​ //初始化蛇的位置,由一个蛇头和两节身体组成,初始位置在游戏界面的中心 head.x = WIDTH / 2; head.y = HEIGHT / 2; board[head.x][head.y] = SNAKEHEAD; ​ body[0].x = head.x - 1; body[0].y = head.y; board[body[0].x][body[0].y] = SNAKEBODY; ​ body[1].x = head.x - 2; body[1].y = head.y; board[body[1].x][body[1].y] = SNAKEBODY; ​ GenerateFood(); //生成随机食物 } ​ //绘制游戏画面 void DrawGame() { system("cls"); //清屏,避免前一帧的内容残留 ​ for (int i = 0; i < HEIGHT; i++) { for (int j = 0; j < WIDTH; j++) { printf("%c", board[j][i]); //输出一个字符 } printf("\n"); //每行输出完后,换行 } ​ printf("Score: %d\n", score); //输出得分 } ​ //更新游戏状态 void UpdateGame() { Move(); //蛇的移动 enum GameState state = CheckGameOver(); //检查游戏是否结束 if (state != Running) //如果游戏结束,则进行相应操作 { GameOver(); } } ​ //检查游戏是否结束 enum GameState CheckGameOver() { //蛇头碰到墙壁,游戏结束 if (board[head.x][head.y] == WALL) return Over; ​ //蛇头碰到蛇身,游戏结束 for (int i = 0; i < length; i++) { if (head.x == body[i].x && head.y == body[i].y) return Over; } ​ //吃到食物后,更新分数和蛇的长度,并生成新的食物 if (head.x == food.x && head.y == food.y) { score += 10; length++; GenerateFood(); } ​ //蛇的长度达到游戏界面总格子数减去墙壁的个数,也就是蛇填满游戏界面,游戏胜利 if (length == (WIDTH - 2) * (HEIGHT - 2) - 4) return Win; ​ return Running; //游戏继续进行 } ​ //生成随机食物 void GenerateFood() { int x, y; do { x = rand() % (WIDTH - 2) + 1; //随机x坐标,排除在边框上的墙壁位置 y = rand() % (HEIGHT - 2) + 1; //随机y坐标,排除在边框上的墙壁位置 } while (board[x][y] != BLANK); //如果随机到的位置不为空白,则重新随机 food.x = x; food.y = y; board[x][y] = FOOD; //在随机位置生成食物 } ​ //蛇的移动 void Move() { //更新蛇身的位置,从后往前移动 for (int i = length - 1; i > 0; i--) { body[i].x = body[i - 1].x; body[i].y = body[i - 1].y; board[body[i].x][body[i].y] = SNAKEBODY; } ​ //更新蛇头的位置 switch (direction) //根据蛇头行进方向进行移动 { case 0: //向右 head.x++; break; case 1: //向下 head.y++; break; case 2: //向左 head.x--; break; case 3: //向上 head.y--; break; } board[head.x][head.y] = SNAKEHEAD; //更新蛇头位置 board[body[length - 1].x][body[length - 1].y] = BLANK; //清除蛇尾 } ​ //用户操作,控制蛇的移动 void Control() { if (kbhit()) //如果有按键按下 { char ch = getch(); //获取按键字符 switch (ch) { case 'w': if (direction != 1) //避免蛇头掉头 direction = 3; break; case 's': if (direction != 3) direction = 1; break; case 'a': if (direction != 0) direction = 2; break; case 'd': if (direction != 2) direction = 0; break; case 'q': GameOver(); //按下'q'键退出游戏 break; } } } ​ //游戏暂停 void Pause() { Sleep(150); //每次循环暂停一段时间,控制蛇的移动速度 } ​ //游戏结束 void GameOver() { system("cls"); //清屏,输出最终得分和游戏结束信息 printf("Game over!\n"); printf("Your score: %d\n", score); exit(0); //直接退出程序 }
  • [技术干货] 通过51单片机实现直流电机调速
    一、项目背景及目的随着各种工业生产设备和机械设备的广泛使用,直流电机调速技术的研究和应用越来越受到人们的重视,具有广泛的应用前景。本项目通过51单片机实现直流电机调速功能,为实际工程应用提供一个可靠和有效的调速方案。二、设计思路(1)系统原理本系统采用PWM(脉冲宽度调制)技术对直流电机进行调速控制。通过改变输出信号的占空比,实现对直流电机的转速控制。系统中包括51单片机、直流电机、电路板以及控制程序。(2)硬件设计电机:使用24V直流电机实现实际转速控制。驱动电路:使用四个寄生二极管三相全桥驱动电路控制电机,使电机可以正反转,并控制电机的速度。51单片机:使用STC89C52单片机,作为控制核心。单片机通过捕捉外部信号和计算控制电压来实现对电机的转速控制。同时还需通过编写程序来控制电机的启动、停止等操作。显示器:使用1602LCD显示屏,显示转速和其他操作信息。电源:使用24V直流电源作为系统的电源。(3)软件设计采用C语言编写单片机程序进行控制。实现PWM技术控制直流电机的转速。通过调整占空比来改变输出电压,从而达到控制直流电机转速的目的。使用定时器模块实现计数来测量电机的转速,并通过显示器实时显示。设定按键和旋钮控制,如启动、停止电机等。三、设计代码 #include <reg52.h> ​ sbit MotorP = P1^0; //定义电机正极口 sbit MotorN = P1^1; //定义电机负极口 float V_motor = 0; //定义电机控制电压 unsigned int speed = 0; //定义电机转速 ​ //初始化函数 void Init() { //定时器计数器及工作模式设置 TMOD |= 0x01; //T0定时器模式1 TH0 = 0xfc; //定时计数最大值,控制PWM频率 TL0 = 0x00; //初值为0 TR0 = 1; //启动T0定时器 ​ //ADC设置 ADC_CONTR = 0x84; //启动AD转换器 } ​ //ADC采样函数 float ADConvert() { ADC_CONTR &= 0xEF; //清除AD转换结束标志位 ADC_CONTR |= 0x40; //启动AD转换 while(!(ADC_CONTR & 0x10)); //等待转换完成 return ADC_RES; //返回转换结果 } ​ //计算电机控制电压函数 void ControlMotor() { unsigned int value = ADConvert(); //采集电位器输出 V_motor = (value / 1023.0) * 5; //根据电压分压公式计算电机控制电压 } ​ //控制电机函数 void DriveMotor() { if(V_motor >= 2.5) //当电位器输出电压大于2.5V时电机正转,当小于2.5V时电机反转 { MotorP = 1; MotorN = 0; } else if(V_motor < 2.5) { MotorP = 0; MotorN = 1; } ​ speed = 60 * 1000 / (3 * TH0 * 12); //根据定时器计数值计算电机转速 } ​ //显示函数 void Display() { //将电机转速和状态信息显示在LCD显示屏上 } ​ //主函数 void main() { Init(); //初始化函数 ​ while(1) { ControlMotor(); //计算电机控制电压 DriveMotor(); //控制电机运行 Display(); //显示电机状态 } } ​