• [技术干货] 通过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(); //显示电机状态 } } ​
  • [技术干货] 基于STM32设计的自动刹车灯
    一、项目介绍随着科技的发展,人们对低碳环保的认知和需求不断提高。骑自行车既能够低碳环保,又能够锻炼身体,成为了很多人出行的首选。然而,由于自行车本身没有带指示灯,比如刹车指示灯等,所以自行车的安全性并不是很好,如果人们在骑自行车时紧急刹车,后车无法及时判断前方自行车的行为,容易造成交通事故。因此,设计一款自动刹车灯系统具有十分重要的意义。本项目实现了通过安装ADXL345陀螺仪和四枚LED灯还有STM32F103C8T6主控芯片来实现自行车自动刹车灯的功能。当自行车上安装了该设备后,ADXL345通过IIC通信协议将X,Y,Z三轴的加速度实时值发送给SMT32F103C8T6主控芯片,并结合STM32高级定时器的PWM功能,输出不同占空比的脉冲,控制不同的LED灯输出多种亮度等级,从而控制不同的LED的开关以及明暗,并且通过不同亮度的红光和绿光混合,能够得到黄色的LED灯光。这样,在自行车急刹或者加速时,实时地控制LED灯的亮度和颜色,让后方车辆能够更清楚地了解前方自行车的行为,从而做出快速的反应,保障骑行者以及后车的安全。同时,该系统也能够提高自行车的可见性,并且对于追求低碳环保的人群来说,让自行车既能低碳环保,又能够锻炼身体。二、设计思路2.1 项目目标本项目通过安装ADXL345陀螺仪和四枚LED灯还有STM32F103C8T6主控芯片来实现自行车自动刹车灯的功能,使得自行车在急刹或者加速时,实时地控制LED灯的亮度和颜色,提高其可见性,降低交通事故的风险。同时,该系统还能够使自行车既能低碳环保,又能够锻炼身体。2.2 项目硬件构成(1)自行车:作为安装系统的物体,需要有一个固定的位置来安装ADXL345陀螺仪和四枚LED灯。(2)ADXL345陀螺仪:通过IIC通信协议与STM32F103C8T6主控芯片通信,并将X、Y、Z三轴的加速度实时值发送给SMT32F103C8T6主控芯片。(3)四枚LED灯:使用不同亮度的红光和绿光混合,能够得到黄色的LED灯光。通过控制其亮度和颜色来提高自行车的可见性。(4)STM32F103C8T6主控芯片:根据接收到的ADXL345数据,结合STN32的高级定时器的PWM功能,输出不同占空比的脉冲,控制不同的LED灯输出多种亮度等级。2.3 项目功能实现(1)自行车加速度监测:ADXL345陀螺仪通过IIC通信协议与STM32F103C8T6主控芯片通信,实时地感知自行车的加速度变化。(2)LED灯亮度和颜色控制:STM32F103C8T6主控芯片运用高级定时器的PWM功能,能够输出不同占空比的脉冲,并控制不同的LED灯输出多种亮度等级,通过不同亮度的红光和绿光混合,能够得到黄色的LED灯光,提高自行车的可见性。(3)系统安装和调试:需要将ADXL345陀螺仪和四枚LED灯与STM32F103C8T6主控芯片连接起来,并进行系统测试和调试。三、系统测试3.1 功能样机安装与焊接绘制好电路原理图之后,按照原理图将自动刹车灯系统的各个模块安装在事先购买好的洞洞板上,然后用导线将他们连接在一起,最后再焊接在一起,做成完整的自动刹车灯电路板。3.2 ADXL345模块调试当上电后,将自动刹车灯电路的串口2外设引脚连接至PC端,将加速度解算后的实际值发送至PC端,通过PC端串口调试助手显示出具体数值,再观察数值是否符合常理。通过显示的数据信息,可以推测出ADXL345陀螺仪能够正常工作。3.3 实物调试最后阶段,将对自行车自动刹车灯进行实物调试,确定其基本功能能够正常实现。当系统上电后,左右各一枚LED发出低亮黄色灯光,如下图。静置30S后,所有LED均熄灭,如下图。当检测到震动后,重新亮起两盏黄色LED灯,如下图。​当检测到刹车时,四枚LED灯均以高亮发出红色灯光,如下图。结合自行车自动刹车灯的功能需求和实物调试结果,可以发现,调试结果完全符合自动刹车灯的预期功能。四、代码设计4.1 主函数 #include "stm32f10x.h" #include "usart.h" #include "led.h" #include "RTC_Time.h" #include <stdio.h> #include "delay.h" #include "sys.h" #include "stdlib.h" #include "stdio.h" #include "string.h" #include "adxl345.h" int main(void) { u32 flag=0; short x, y, z; float accelerated; LED_GPIO_Config();//初始化LED USART2_Config(); delay_init(); //延时函数初始化 PWM_LED_INIT(); //PWM PA8-9 LED_Init(); //PB7 LED-R PBout(7) = 1; ADXL345_Init(); //PB 10,11 ADXL345_Read_Average(&x, &y, &z, 20); ADXL345_AUTO_Adjust((char *)&x, (char *)&y, (char *)&z); TIM_SetCompare1(TIM1, 50); //设置TIMx捕获比较1寄存器(通道1)值(脉冲宽度) 占空比%20 TIM_SetCompare2(TIM1, 50); //设置TIMx捕获比较2寄存器(通道2)值(脉冲宽度) 占空比%20 while (1) { ADXL345_Read_Average(&x, &y, &z, 5); //读加速度值 accelerated=(x*3.9/1000*9.8); //加速度实际值 printf("X=%4.1f Y=%4.1f Z=%4.1f\r\n",accelerated,(y*3.9/1000*9.8),(z*3.9/1000*9.8)); while(flag>425) { TIM_SetCompare1(TIM1, 0); //通道2 占空比%0 TIM_SetCompare2(TIM1, 0); //通道2 占空比%0 ADXL345_Read_Average(&x, &y, &z, 5); accelerated=(x*3.9/1000*9.8); if(accelerated<-5||accelerated>5) { break; } } flag++; if(accelerated<-4) { //四个LED低电平导通 TIM_SetCompare1(TIM1, 0); //GREEN不亮 TIM_SetCompare2(TIM1, 1000); //RED高亮 PBout(7) = 0; flag=0; } if(accelerated>0) { PBout(7) = 1; TIM_SetCompare1(TIM1, 50); //RED低亮 TIM_SetCompare2(TIM1, 50); //GREEN低亮 } if(accelerated>5) { flag=0; } } }4.2 LED灯控制 #include "led.h" #include "delay.h" void LED_GPIO_Config(void) { //定义一个GPIO_InitTypeDef 类型的结构体,名字叫GPIO_InitStructure GPIO_InitTypeDef GPIO_InitStructure; //使能GPIOC的外设时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC,ENABLE); //选择要用的GPIO引脚 GPIO_InitStructure.GPIO_Pin =GPIO_Pin_13; ///设置引脚模式为推免输出模式 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //设置引脚速度为50MHZ GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; //调用库函数,初始化GPIO GPIO_Init(GPIOC, &GPIO_InitStructure); } void TIME_INIT() { TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure; TIM_OCInitTypeDef TIM_OCInitStructure; //根据TIM_OCInitStruct中指定的参数初始化外设TIMx RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1, ENABLE); //TIM1定时器初始化 10ms TIM_TimeBaseInitStructure.TIM_Period = 999; TIM_TimeBaseInitStructure.TIM_Prescaler = 719; TIM_TimeBaseInitStructure.TIM_ClockDivision = 0; TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM1, &TIM_TimeBaseInitStructure); //TIM1的PWM配置 TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_Pulse = 0;//设置初始PWM脉冲宽度为0 TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; //PWM输出使能 TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_Low;//当定时器计数值小于CCR_Val时为低电平 //通道的使能 TIM_OC1Init(TIM1, &TIM_OCInitStructure); //通道1 TIM_OC1PreloadConfig(TIM1, TIM_OCPreload_Enable); TIM_OC2Init(TIM1, &TIM_OCInitStructure); //通道2 TIM_OC2PreloadConfig(TIM1, TIM_OCPreload_Enable); TIM_ARRPreloadConfig(TIM1, ENABLE); //使能TIM1重载寄存器ARR TIM_Cmd(TIM1, ENABLE); //使能 TIM_CtrlPWMOutputs(TIM1, ENABLE); //高级定时器必须加 } void PWM_LED_INIT(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB, ENABLE); //GPIOA8,9,10是TIM1的通道1,2,3 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8 | GPIO_Pin_9 ; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽输出 GPIO_Init(GPIOA, &GPIO_InitStructure); TIME_INIT(); } void LED_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; //RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_GPIOB,ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_Init(GPIOB, &GPIO_InitStructure); } ​4.3 adxl345.c #include "adxl345.h" #include "sys.h" #include "delay.h" #include "math.h" u8 ADXL345_Init(void) { IIC_Init(); //初始化IIC总线 if(ADXL345_RD_Reg(DEVICE_ID)==0XE5) //读取器件ID { ADXL345_WR_Reg(0X31,0X2B); //低电平中断输出,13位全分辨率,输出数据右对齐,16g量程 ADXL345_WR_Reg(0X2C,0x0A); //数据输出速度为100Hz ADXL345_WR_Reg(0X2D,0x28); //链接使能,测量模式 ADXL345_WR_Reg(0X2E,0x00); //不使用中断 ADXL345_WR_Reg(0X1E,0x00); ADXL345_WR_Reg(0X1F,0x00); ADXL345_WR_Reg(0X20,0x00); return 0; } return 1; } //写ADXL345寄存器 //addr:寄存器地址 //val:要写入的值 //返回值:无 void ADXL345_WR_Reg(u8 addr,u8 val) { IIC_Start(); IIC_Send_Byte(ADXL_WRITE); //发送写器件指令 IIC_Wait_Ack(); IIC_Send_Byte(addr); //发送寄存器地址 IIC_Wait_Ack(); IIC_Send_Byte(val); //发送值 IIC_Wait_Ack(); IIC_Stop(); //产生一个停止条件 } //读ADXL345寄存器 //addr:寄存器地址 //返回值:读到的值 u8 ADXL345_RD_Reg(u8 addr) { u8 temp=0; IIC_Start(); IIC_Send_Byte(ADXL_WRITE); //发送写器件指令 temp=IIC_Wait_Ack(); IIC_Send_Byte(addr); //发送寄存器地址 temp=IIC_Wait_Ack(); IIC_Start(); //重新启动 IIC_Send_Byte(ADXL_READ); //发送读器件指令 temp=IIC_Wait_Ack(); temp=IIC_Read_Byte(0); //读取一个字节,不继续再读,发送NAK IIC_Stop(); //产生一个停止条件 return temp; //返回读到的值 } //读取ADXL的平均值 //x,y,z:读取10次后取平均值 void ADXL345_RD_Avval(short *x,short *y,short *z) { short tx=0,ty=0,tz=0; u8 i; for(i=0;i<10;i++) { ADXL345_RD_XYZ(x,y,z); delay_ms(10); tx+=(short)*x; ty+=(short)*y; tz+=(short)*z; } *x=tx/10; *y=ty/10; *z=tz/10; } //自动校准 //xval,yval,zval:x,y,z轴的校准值 void ADXL345_AUTO_Adjust(char *xval,char *yval,char *zval) { short tx,ty,tz; u8 i; short offx=0,offy=0,offz=0; ADXL345_WR_Reg(POWER_CTL,0x00); //先进入休眠模式. delay_ms(100); ADXL345_WR_Reg(DATA_FORMAT,0X2B); //低电平中断输出,13位全分辨率,输出数据右对齐,16g量程 ADXL345_WR_Reg(BW_RATE,0x0A); //数据输出速度为100Hz ADXL345_WR_Reg(POWER_CTL,0x28); //链接使能,测量模式 ADXL345_WR_Reg(INT_ENABLE,0x00); //不使用中断 ADXL345_WR_Reg(OFSX,0x00); ADXL345_WR_Reg(OFSY,0x00); ADXL345_WR_Reg(OFSZ,0x00); delay_ms(12); for(i=0;i<10;i++) { ADXL345_RD_Avval(&tx,&ty,&tz); offx+=tx; offy+=ty; offz+=tz; } offx/=10; offy/=10; offz/=10; *xval=-offx/4; *yval=-offy/4; *zval=-(offz-256)/4; ADXL345_WR_Reg(OFSX,*xval); ADXL345_WR_Reg(OFSY,*yval); ADXL345_WR_Reg(OFSZ,*zval); } //读取3个轴的数据 //x,y,z:读取到的数据 void ADXL345_RD_XYZ(short *x,short *y,short *z) { u8 buf[6]; u8 i; IIC_Start(); IIC_Send_Byte(0X3A); //发送写器件指令 IIC_Wait_Ack(); IIC_Send_Byte(0x32); //发送寄存器地址(数据缓存的起始地址为0X32) IIC_Wait_Ack(); IIC_Start(); //重新启动 IIC_Send_Byte(0X3B); //发送读器件指令 IIC_Wait_Ack(); for(i=0;i<6;i++) { if(i==5)buf[i]=IIC_Read_Byte(0);//读取一个字节,不继续再读,发送NACK else buf[i]=IIC_Read_Byte(1); //读取一个字节,继续读,发送ACK delay_us(15); IIC_Start(); //重新启动 IIC_Send_Byte(0X3A); //发送写器件指令 IIC_Wait_Ack(); IIC_Send_Byte(0x33+i); //发送寄存器地址(数据缓存的起始地址为0X32) IIC_Wait_Ack(); IIC_Start(); //重新启动 IIC_Send_Byte(0X3B); //发送读器件指令 IIC_Wait_Ack(); } IIC_Stop(); //产生一个停止条件 *x=(short)(((u16)buf[1]<<8)+buf[0]); *y=(short)(((u16)buf[3]<<8)+buf[2]); *z=(short)(((u16)buf[5]<<8)+buf[4]); } //读取ADXL345的数据times次,再取平均 //x,y,z:读到的数据 //times:读取多少次 void ADXL345_Read_Average(short *x,short *y,short *z,u8 times) { u8 i; short tx,ty,tz; *x=0; *y=0; *z=0; if(times)//读取次数不为0 { for(i=0;i<times;i++)//连续读取times次 { ADXL345_RD_XYZ(&tx,&ty,&tz); *x+=tx; *y+=ty; *z+=tz; delay_ms(5); } *x/=times; *y/=times; *z/=times; } } //得到角度 //x,y,z:x,y,z方向的重力加速度分量(不需要单位,直接数值即可) //dir:要获得的角度.0,与Z轴的角度;1,与X轴的角度;2,与Y轴的角度. //返回值:角度值.单位0.1°. short ADXL345_Get_Angle(float x,float y,float z,u8 dir) { float temp; float res=0; switch(dir) { case 0://与自然Z轴的角度 temp=sqrt((x*x+y*y))/z; res=atan(temp); break; case 1://与自然X轴的角度 temp=x/sqrt((y*y+z*z)); res=atan(temp); break; case 2://与自然Y轴的角度 temp=y/sqrt((x*x+z*z)); res=atan(temp); break; } return res*1800/3.14; }
  • [专题汇总] 7月嵌入式项目开发专题总汇
    一、前言当前文章总结了14个基于不同单片机(51单片机和STM32)设计的软件和硬件项目。这些项目涵盖了计算器、手机、酒精检测仪、环境温度与湿度检测设备、考勤系统、门禁照相机、人体健康检测仪、数码相册、太阳能热水器、数显热水器和智能空调等多个领域。这些项目展示了单片机在不同领域的应用。通过这些项目,可以学习到如何设计和开发基于单片机的硬件和软件系统。其中,基于51单片机和STM32的项目涉及了计算器、手机、酒精检测仪、环境温度与湿度检测设备、考勤系统、门禁照相机、人体健康检测仪、数码相册、太阳能热水器、数显热水器和智能空调等多个领域。这些项目不仅展示了单片机的强大功能,还提供了实际应用的示例和参考。无论是初学者还是有经验的开发者,都可以从这些项目中获得有价值的知识和经验,进一步拓展自己在单片机开发领域的能力。二、项目实例【1】基于51单片机设计的计算器cid:link_3 项目里采用了单片机的IO口、定时器和LCD1602显示屏等技术原理。其中,IO口用于控制矩阵键盘、蜂鸣器和LCD1602显示屏等外设;定时器用于进行键盘扫描,确保能够准确地捕捉到按键的输入;LCD1602显示屏用于显示输入的数字和计算结果。设计思路主要分为三个部分:键盘扫描、计算器运算和LCD1602显示。在键盘扫描部分,通过定时器中断的方式进行键盘扫描,判断是否有按键按下,并将按键对应的数字保存到缓存区中。在计算器运算部分,采用栈的数据结构进行计算器运算,当按下运算符号时,将之前输入的数字压入栈中,等待下一次输入。当按下“=”时,从栈中取出数字进行计算,并将结果保存到栈中。最后将结果从栈中取出,显示在LCD1602显示屏上。在LCD1602显示部分,通过设置LCD1602的命令和数据,可以实现在LCD1602上显示数字和运算符号等内容。【2】基于STM32设计的简易手机cid:link_4 基于STM32设计的简易手机可以作为智能手表的模型进行开发,方便老人和儿童佩戴。项目主要是为了解决老年人或儿童使用智能手表时可能遇到的困难,例如操作困难、功能复杂等问题。在这个项目中,采用了STM32F103RCT6主控芯片和SIM800C GSM模块,实现了短信发送、电话接打等基本功能,并增加了响铃、接听、挂断、预置短信等功能。当检测到新的电话来时,会通过蜂鸣器通知用户,并通过按键进行接电话和挂电话,使操作更加简单易懂。手机还提供4个按键,可以向预先指定的联系人发送4条预置短信,更方便快捷。【3】基于STM32设计的酒精检测仪cid:link_5 随着社会的发展和生活水平的提高,人们对于行车安全、家庭安全的要求越来越高,而酒驾等问题也日渐突出,为此,开发一款基于STM32的酒精检测仪,通过检测酒精浓度,实时显示结果并进行报警,可以有效避免因酒后驾车带来的安全隐患。【4】基于51单片机+SHT30设计的环境温度与湿度检测设备(IIC模拟时序)cid:link_6 当前文章介绍基于51单片机和SHT30传感器设计的环境温度与湿度检测设备。设备采用IIC模拟时序通信协议,能够实时监测环境的温度和湿度,并将数据通过LCD显示屏显示出来;可以广泛应用于室内环境监测、气象观测、农业温室监测等领域。在本项目中,使用了51单片机作为主控芯片,SHT30传感器作为温湿度传感器,LCD显示屏作为数据显示模块。通过51单片机的GPIO口模拟IIC通信协议,实现了与SHT30传感器的数据通信。【5】基于STM32+SHT30设计的环境温度与湿度检测系统(IIC模拟时序)cid:link_7当前介绍基于STM32F103ZCT6芯片设计的环境温度与湿度检测系统设计过程。当前系统通过SHT30温湿度传感器采集环境温度和湿度数据,并通过模拟IIC时序协议将数据传输到STM32芯片上。然后,STM32芯片通过处理这些数据并将它们显示在0.91寸OLED显示屏上,以便用户能够方便地观察环境温度和湿度的变化情况。系统的主控芯片采用了STM32F103ZCT6,这是一款高性能的32位ARM Cortex-M3微控制器,具有丰富的外设和存储器资源,可满足各种应用的需求。温湿度检测传感器采用了SHT30,这是一款高精度的数字式温湿度传感器,具有快速响应、低功耗、高可靠性等特点。【6】Qt使用kingbase数据库存储数据(完成考勤系统数据增删改查)cid:link_8 当前基于Qt(C++)开发了一款教室上课考勤系统的软件,主要是使用了Kingbase数据库进行数据存储和管理。完成的具体功能如下:(1)功能齐全:软件可以完成学生、教师和管理员的登陆和注册,教师可以发布课程信息和考勤信息,学生可以查看自己的课程信息和考勤记录,管理员可以对教师和学生信息进行管理。软件具有数据可视化等功能,方便管理员直观地了解教学情况。(2)高效稳定:采用了Kingbase数据库存储数据,保证了数据存储的可靠性和一致性,同时也提高了系统性能和响应速度。在程序设计方面采用了MVC模式,将程序的逻辑与界面分离,使得程序结构清晰,易于维护和扩展。(3)用户友好:采用了人性化的操作界面和交互方式,让用户能够方便地浏览和管理课程和考勤记录。考虑到了软件的安全性问题,采用了哈希加密算法保护用户密码。【7】C语言里变量的生命周期cid:link_0 在 C 语言中,变量的生命周期指的是该变量存在的时间段,理解变量的内存释放时机,设计程序才能少出问题。在程序执行期间,变量会经历以下三个阶段:(1)定义阶段(定义变量):在定义变量时,编译器会为该变量分配内存空间。此时变量的值是不确定的。(2)使用阶段(赋值、读取变量):在程序执行过程中,可以对变量进行赋值或读取操作。此时变量的值是确定的,并且会随着程序执行的进度而变化。(3)销毁阶段(变量被销毁):在变量的作用域结束时,该变量就会被销毁。在这个过程中,编译器会自动释放该变量所占用的内存空间。【8】SQLite数据库完成数据增删改查cid:link_9当前文章介绍的设计的主要功能是利用 SQLite 数据库实现宠物投喂器上传数据的存储,并且支持数据的增删改查操作。其中,宠物投喂器上传的数据包括投喂间隔时间、水温、剩余重量等参数。实现功能:创建 SQLite 数据库表,用于存储宠物投喂器上传的数据。实现对数据库表中数据的插入操作,即将从宠物投喂器接收到的数据存储到数据库中。实现对数据库表中数据的查询操作,包括按照投喂间隔时间、水温、剩余重量等参数进行筛选,以便用户能够查看特定范围内的数据信息。实现对数据库表中数据的修改操作,即可以修改已经存储的宠物投喂器上传的数据。实现对数据库表中数据的删除操作,即可以删除已经存储的宠物投喂器上传的数据。【9】基于STM32设计的门禁照相机cid:link_1 当前文章介绍基于STM32设计的门禁照相机,本项目提供了一种更加智能、安全、便捷的门禁解决方案。门禁照相机采用STM32F103ZET6 MCU作为主控芯片,配合2.8寸LCD显示屏、OV7725数字摄像头、SD卡和模拟门铃按键等外设模块,实现了摄像头画面实时显示、门铃触发拍照、图片存储等功能。在使用该门禁照相机时,来访客人只需按下门铃按键,摄像头即可自动拍摄照片并保存到SD卡中。同时,用户也可以通过LCD屏幕进行时间调整和本地图片浏览等操作,提高了门禁系统的可操作性和用户体验。门禁照相机的设计为了提高门禁系统的安全性和智能化程度,解决传统门禁系统存在的诸多问题。通过采用数字摄像头替代传统猫眼,并实现照片自动拍摄和存储功能,有效提高了门禁系统的安全性。同时,通过LCD屏幕进行时间调整和本地图片浏览等操作,实现了门禁系统的智能化,提高了用户的使用体验。【10】基于STM32设计的人体健康检测仪cid:link_10 当前文章介绍基于STM32设计的人体健康检测仪。设备采用STM32系列MCU作为主控芯片,配备血氧浓度传感器(使用MAX30102血氧浓度检测传感器)、OLED屏幕和电池供电等外设模块。设备可以广泛应用于医疗、健康等领域。可以帮助医生和病人更好地了解病情变化,提高治疗效果和生活质量。设备也可以用于健康管理、运动监测等场景,帮助用户了解自己的身体状况,保持健康的生活方式。在项目中,使用了KEIL作为开发平台和工具,通过血氧模块采集人体的心跳和血氧浓度参数,并通过OLED屏幕显示现在的心跳和血氧浓度。同时,通过指标分析,提供采集到的数据与正常指标比对,分析被检测人员的健康状态。采集的数据可通过蓝牙或者WIFI传递给手机APP进行处理,方便用户随时了解自己的身体状况。本设计采用STM32为主控芯片,搭配血氧浓度传感器和OLED屏幕,实现了人体健康数据的采集和展示,并对采集到的数据进行分析,判断被检测人员的健康状态。同时,设计使用蓝牙或WiFi将采集到的数据传递给手机APP进行处理。【11】基于STM32设计的数码相册cid:link_2 项目是基于STM32设计的数码相册,能够通过LCD显示屏解码显示主流的图片,支持bmp、jpg、gif等格式。用户可以通过按键或者触摸屏来切换图片,同时还可以旋转显示,并能够自适应居中显示,小尺寸图片居中显示,大尺寸图片自动缩小显示(超出屏幕范围)。图片从SD卡中获取。【12】基于STM32设计的太阳能热水器cid:link_11 本项目使用 STM32F103C8T6 微控制器作为核心处理器,结合多个传感器和执行器,实现了太阳能热水器的自动控制。通过对光照、温度、水位等各种参数的监测和分析,对水泵、电磁阀等设备进行自动控制,从而实现太阳能热水器的高效、安全、可靠运行。【13】基于STM32设计的数显热水器cid:link_12当前介绍的项目是基于 STM32F103ZET6 系列 MCU 设计的数显热水器,通过显示屏来显示热水器的温度及其工作状态,通过 PT100 传感器来检测热水器的温度变化,并通过电加热片实现加热过程,以达到控制热水器温度的目的。【14】基于STM32设计的智能空调cid:link_13 随着人们生活水平的不断提高,对居住环境的舒适度要求也越来越高。空调作为一种重要的家电设备,已经成为了现代家庭中必不可少的一部分。本文介绍了一种基于STM32的智能空调设计方案,可以自动地根据环境温度进行温度调节。
  • [技术干货] 基于STM32设计的智能空调
    一、项目背景随着人们生活水平的不断提高,对居住环境的舒适度要求也越来越高。空调作为一种重要的家电设备,已经成为了现代家庭中必不可少的一部分。本文介绍了一种基于STM32的智能空调设计方案,可以自动地根据环境温度进行温度调节。最终的成品的模型如下:二、设计思路2.1 整体构架智能空调系统由温度检测传感器、微控制器、OLED显示屏、按键及直流电源等组件构成。传感器用于检测环境温度,通过微控制器进行处理后,将结果输出到OLED显示屏上展示。按键可根据需求调整预设阀值,切换模式等操作。2.2 硬件设计(1)温度检测传感器选择DS18B20数字温度传感器作为本系统的温度检测器件。该传感器具有精度高,响应速度快等特点,可以满足该系统的检测需求。(2)微控制器使用STM32F103系列的微控制器,在该控制器活跃的生态环境下,以及其先进的处理能力,可以对信号进行快速采集、处理和控制。(3)OLED显示屏本系统使用的是一块128 * 64 OLED显示屏,显示屏具有高亮度、高对比度和低功耗等优点,易于与STM32微控制器进行通信。2.3 软件设计在软件设计方面,实现了温度检测传感器数据的采集,使用处理算法对数据进行处理,根据预设阀值自动调节温度,同时可以根据用户需求,切换制冷、制热和关闭等3种模式。最后,将结果通过OLED显示屏进行输出。三、代码设计3.1 DS18B20温度检测代码 #include "main.h" #include "delay.h" ​ #define GPIO_PORT_TEMP GPIOA //温度数据引脚所在的端口 #define GPIO_PIN_TEMP GPIO_Pin_0 //温度数据引脚所在的引脚编号 ​ #define RCC_PORT_TEMPP RCC_APB2Periph_GPIOA // 温度引脚所在端口时钟号 ​ void USART_SendByte( USART_TypeDef * pUSARTx, uint8_t ch ); ​ void delay_us(uint32_t us){ // 延时us微秒函数 uint8_t i; for(i=0;i<us;i++){ asm("nop"); } } ​ float get_temp(){ // 获取温度函数 uint16_t temp; uint8_t buf[2]; ​ GPIO_InitTypeDef GPIO_InitStruct; TIM_TimeBaseInitTypeDef TIM_InitStruct; ​ RCC_APB2PeriphClockCmd(RCC_PORT_TEMPP,ENABLE); ​ //DATA拉低480us复位 GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStruct.GPIO_Pin = GPIO_PIN_TEMP; GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIO_PORT_TEMP , &GPIO_InitStruct); GPIO_ResetBits(GPIO_PORT_TEMP , GPIO_PIN_TEMP ); delay_us(500); GPIO_SetBits(GPIO_PORT_TEMP , GPIO_PIN_TEMP ); delay_us(60); ​ //查询DS18B20是否存在 GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU; GPIO_InitStruct.GPIO_Pin = GPIO_PIN_TEMP; GPIO_Init(GPIO_PORT_TEMP , &GPIO_InitStruct); while (GPIO_ReadInputDataBit(GPIO_PORT_TEMP , GPIO_PIN_TEMP ) == RESET); ​ //通信开始 GPIO_ResetBits(GPIO_PORT_TEMP , GPIO_PIN_TEMP ); delay_us(480); GPIO_SetBits(GPIO_PORT_TEMP , GPIO_PIN_TEMP ); delay_us(60); ​ //读取温度数据 GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU; GPIO_InitStruct.GPIO_Pin = GPIO_PIN_TEMP ; GPIO_Init(GPIO_PORT_TEMP , &GPIO_InitStruct); delay_us(10); if (GPIO_ReadInputDataBit(GPIO_PORT_TEMP , GPIO_PIN_TEMP ) == RESET){ temp |=0x01; } else{ temp &=0xfe; } delay_us(50); if (GPIO_ReadInputDataBit(GPIO_PORT_TEMP , GPIO_PIN_TEMP ) == RESET){ temp |=0x02; } else{ temp &=0xfd; } delay_us(50); if (GPIO_ReadInputDataBit(GPIO_PORT_TEMP , GPIO_PIN_TEMP ) == RESET){ temp |=0x04; } else{ temp &=0xfb; } delay_us(50); if (GPIO_ReadInputDataBit(GPIO_PORT_TEMP , GPIO_PIN_TEMP ) == RESET){ temp |=0x08; } else{ temp &=0xf7; } delay_us(50); if (GPIO_ReadInputDataBit(GPIO_PORT_TEMP , GPIO_PIN_TEMP ) == RESET){ temp |=0x10; } else{ temp &=0xef; } delay_us(50); if (GPIO_ReadInputDataBit(GPIO_PORT_TEMP , GPIO_PIN_TEMP ) == RESET){ temp |=0x20; } else{ temp &=0xdf; } delay_us(50); if (GPIO_ReadInputDataBit(GPIO_PORT_TEMP , GPIO_PIN_TEMP ) == RESET){ temp |=0x40; } else{ temp &=0xbf; } delay_us(50); if (GPIO_ReadInputDataBit(GPIO_PORT_TEMP , GPIO_PIN_TEMP ) == RESET){ temp |=0x80; } else{ temp &=0x7f; } delay_us(50); ​ //读取温度小数点数据 if (GPIO_ReadInputDataBit(GPIO_PORT_TEMP , GPIO_PIN_TEMP ) == RESET){ buf[0] |=0x01; } else{ buf[0] &=0xfe; } delay_us(50); if (GPIO_ReadInputDataBit(GPIO_PORT_TEMP , GPIO_PIN_TEMP ) == RESET){ buf[0] |=0x02; } else{ buf[0] &=0xfd; } delay_us(50); if (GPIO_ReadInputDataBit(GPIO_PORT_TEMP , GPIO_PIN_TEMP ) == RESET){ buf[0] |=0x04; } else{ buf[0] &=0xfb; } delay_us(50); if (GPIO_ReadInputDataBit(GPIO_PORT_TEMP , GPIO_PIN_TEMP ) == RESET){ buf[0] |=0x08; } else{ buf[0] &=0xf7; } delay_us(50); if (GPIO_ReadInputDataBit(GPIO_PORT_TEMP , GPIO_PIN_TEMP ) == RESET){ buf[0] |=0x10; } else{ buf[0] &=0xef; } delay_us(50); if (GPIO_ReadInputDataBit(GPIO_PORT_TEMP , GPIO_PIN_TEMP ) == RESET){ buf[0] |=0x20; } else{ buf[0] &=0xdf; } delay_us(50); if (GPIO_ReadInputDataBit(GPIO_PORT_TEMP , GPIO_PIN_TEMP ) == RESET){ buf[0] |=0x40; } else{ buf[0] &=0xbf; } delay_us(50); if (GPIO_ReadInputDataBit(GPIO_PORT_TEMP , GPIO_PIN_TEMP ) == RESET){ buf[0] |=0x80; } else{ buf[0] &=0x7f; } delay_us(50); ​ return (float)temp+((float)buf[0]/16.0); // 将温度整数位和小数位转换为十进制 } ​ int main(void){ ​ char temp_buf[20]; // 接收温度值的临时缓冲区 ​ USART_InitTypeDef USART_InitStruct; ​ RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE); RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1,ENABLE); ​ USART_InitStruct.USART_BaudRate = 115200; USART_InitStruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None; USART_InitStruct.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; USART_InitStruct.USART_Parity = USART_Parity_No; USART_InitStruct.USART_StopBits = USART_StopBits_1; USART_InitStruct.USART_WordLength = USART_WordLength_8b; USART_Init(USART1,&USART_InitStruct); ​ USART_Cmd(USART1,ENABLE); ​ while(1){ float temp_get=get_temp(); // 获取当前温度值 sprintf(temp_buf,"temp:%0.1f\r\n",temp_get); // 将温度值格式化为字符串输出 for(int i=0;i<strlen(temp_buf);i++){ // 逐字符发送温度值至串口 USART_SendByte(USART1,temp_buf[i]); } delay_ms(1000); // 延时1s后再次获取温度值 } } ​ void USART_SendByte( USART_TypeDef * pUSARTx, uint8_t ch ){ while(USART_GetFlagStatus(pUSARTx,USART_FLAG_TXE) == RESET); USART_SendData(pUSARTx,ch); }3.2 OLED显示屏代码 #include "main.h" #include "delay.h" #include "oled.h" ​ void iic_init(void); void GPIO_I2C_Delay(void); void write_com(unsigned char com); void write_data(unsigned char data); ​ int main(void){ ​ unsigned char x,y; iic_init(); // 初始化IIC接口 OLED_Init(); // 初始化OLED显示屏 ​ while(1){ OLED_ShowString(0,0,"1234"); // 在OLED显示屏上显示字符串“1234” delay_ms(500); // 延时500ms OLED_Clear(); // 清空OLED显示屏 } } ​ void iic_init(void){ GPIO_InitTypeDef GPIO_InitStruct; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); //GPIOB使能 RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE); //I2C1使能 ​ GPIO_InitStruct.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7; GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_OD; //配置开漏输出 GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &GPIO_InitStruct); ​ I2C_InitTypeDef I2C_InitStruct; I2C_DeInit(I2C1); ​ I2C_InitStruct.I2C_Mode = I2C_Mode_I2C; // I2C 模式 I2C_InitStruct.I2C_DutyCycle = I2C_DutyCycle_2; // 数传比率 2 I2C_InitStruct.I2C_OwnAddress1 = 0x00; // 地址1, 设备地址 I2C_InitStruct.I2C_Ack = I2C_Ack_Enable; // 开启I2C应答机制 I2C_InitStruct.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit; //设备地址长度为 7 位 I2C_InitStruct.I2C_ClockSpeed = 400000; // 时钟速度为400kHz I2C_Cmd(I2C1, ENABLE); ​ I2C_Init(I2C1, &I2C_InitStruct); } ​ void GPIO_I2C_Delay(void){ uint32_t i = 1000; while(i--); } ​ void write_com(unsigned char com){ while(I2C_GetFlagStatus(I2C1,I2C_FLAG_BUSY)); //等待I2C总线空闲 I2C_GenerateSTART(I2C1,ENABLE); //发送起始信号 while(!I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_MODE_SELECT)); I2C_Send7bitAddress(I2C1,0x78,I2C_Direction_Transmitter);//选择写入模式,发送从机器OLED的地址0x78 while(!I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)); I2C_SendData(I2C1,0x00); //发送控制字节0x00表示写入指令 while(!I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_BYTE_TRANSMITTED)); I2C_SendData(I2C1,com); //写入要发送的指令 while(!I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_BYTE_TRANSMITTED)); I2C_GenerateSTOP(I2C1,ENABLE); //停止信号,传输结束 } ​ void write_data(unsigned char data){ while(I2C_GetFlagStatus(I2C1,I2C_FLAG_BUSY)); //等待I2C总线空闲 I2C_GenerateSTART(I2C1,ENABLE); //发送起始信号 while(!I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_MODE_SELECT)); I2C_Send7bitAddress(I2C1,0x78,I2C_Direction_Transmitter); //选择写入模式,发送从机器OLED的地址0x78 while(!I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)); I2C_SendData(I2C1,0x40); //发送控制字节0x40表示写入数据 while(!I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_BYTE_TRANSMITTED)); I2C_SendData(I2C1,data); //写入要发送的数据 while(!I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_BYTE_TRANSMITTED)); I2C_GenerateSTOP(I2C1,ENABLE); //停止信号,传输结束 }
  • [技术干货] 基于STM32设计的太阳能热水器
    一、概述本项目使用 STM32F103C8T6 微控制器作为核心处理器,结合多个传感器和执行器,实现了太阳能热水器的自动控制。通过对光照、温度、水位等各种参数的监测和分析,对水泵、电磁阀等设备进行自动控制,从而实现太阳能热水器的高效、安全、可靠运行。二、硬件设计(1)模块组成太阳能热水器模块主要由以下几个部分组成:光敏传感器模块:用于检测阳光强度,反映太阳辐射强度和方向。温度传感器模块:用于检测太阳能集热器表面和水箱内的温度,并根据温度变化调整水泵、电磁阀等设备的运行状态。液位传感器模块:用于检测水箱内的液位,并根据液位高低控制水泵和电磁阀的启停。水泵模块:通过控制水泵的启停,实现水循环流动和充水功能。电磁阀模块:通过控制电磁阀的开关,实现热水器的放水和接水功能。(2)硬件连接其中,光敏传感器模块、温度传感器模块和液位传感器模块通过 ADC 接口与 STM32F103C8T6 微控制器进行连接;水泵模块和电磁阀模块则通过 GPIO 口控制。连接方式如下:光敏传感器模块:将光敏传感器输出口与 ADC1 通道10 连接,并用一个电位器调整 ADC 的参考电压,使其范围在 0-3.3V 之间。温度传感器模块:将 DS18B20 温度传感器数据线与 GPIOA 的 PA8 引脚连接,并将 VCC 和 GND 分别接到 3.3V 和 GND。液位传感器模块:将液位传感器输出口与 ADC1 通道11 连接,并用一个电位器调整 ADC 的参考电压。水泵模块:将水泵正极接到 GPIOB 的 PB1 引脚,将负极接到电源的负极。电磁阀模块:将电磁阀正极接到 GPIOB 的 PB0 引脚,将负极接到电源的负极。三、软件设计3.1 任务分配整个项目采用 FreeRTOS 系统进行开发,实现数数的监测和控制,开发以下几个任务:光敏传感器任务:定时读取光敏传感器输出口的电压值,并进行数据处理,得到当前的光照强度。温度传感器任务:定时向 DS18B20 温度传感器发送温度采样请求,接收并解析响应数据,得到当前的太阳能集热器表面温度和水箱内温度。液位传感器任务:定时读取液位传感器输出口的电压值,并进行数据处理,得到当前的水箱水位高度。控制任务:根据光照强度、温度和水位高度等参数,决定是否需要启动水泵或电磁阀等设备。伪代码如下: void Light_Sensor_Task(void) { while (1) { voltage = ADC_Get_Voltage(); // 获取光敏传感器输出电压 light_intensity = voltage * 100 / 3.3f; // 根据电压计算光照强度 vTaskDelay(1000); // 延时 1s } } ​ void Temperature_Sensor_Task(void) { while (1) { DS18B20_Start_Conversion(); // 向温度传感器发送采样请求 temperature1 = DS18B20_Read_Temperature(); // 读取太阳能集热器表面温度 temperature2 = DS18B20_Read_Temperature(); // 读取水箱内温度 vTaskDelay(1000); // 延时 1s } } ​ void Water_Level_Sensor_Task(void) { while (1) { voltage = ADC_Get_Voltage(); // 获取液位传感器输出电压 water_level = voltage * 100 / 3.3f; // 根据电压计算水位高度 vTaskDelay(1000); // 延时 1s } } ​ void Control_Task(void) { while (1) { if (light_intensity > THRESHOLD && temperature1 > THRESHOLD && water_level > THRESHOLD) // 如果各种参数均符合要求,则启动水泵和电磁阀 { GPIO_SetBits(GPIOB, GPIO_Pin_1); // 启动水泵 GPIO_ResetBits(GPIOB, GPIO_Pin_0); // 关闭电磁阀 } else // 否则关闭水泵,打开电磁阀,放水 { GPIO_ResetBits(GPIOB, GPIO_Pin_1); // 关闭水泵 GPIO_SetBits(GPIOB, GPIO_Pin_0); // 启动电磁阀 } vTaskDelay(1000); // 延时 1s } }3.2 光敏传感器任务 /* 光敏传感器任务 */ void Light_Sensor_Task(void *pvParameters) { uint16_t adc_value; ​ while (1) { /* 读取 ADC 值并计算光照强度 */ if (HAL_ADC_Start(&hadc1) == HAL_OK) { if (HAL_ADC_PollForConversion(&hadc1, 10) == HAL_OK) { adc_value = HAL_ADC_GetValue(&hadc1); light_intensity = adc_value * 3300 / 4096.0; } } ​ vTaskDelay(pdMS_TO_TICKS(1000)); // 延时 1s } }在函数中,声明一个变量 adc_value 用于存储读取到的 ADC 值。使用 if 条件语句检查 ADC 是否成功启动,并且使用 HAL_ADC_PollForConversion() 函数判断当前转换是否完成,如果转换完成,就获取 ADC 值,并且通过简单的计算公式将 ADC 值转换为光照强度值,最后将结果存储在 light_intensity 变量中。3.3 温度传感器任务 /* 温度传感器任务 */ void Temperature_Sensor_Task(void *pvParameters) { float temperature; ​ /* 初始化 DS18B20 */ DS18B20_Init(&htim2, GPIOA, GPIO_PIN_10); ​ while (1) { /* 读取温度值 */ temperature = DS18B20_Read_Temperature(); ​ /* 将读取到的温度值存储在全局变量中 */ current_temperature = temperature; ​ vTaskDelay(pdMS_TO_TICKS(1000)); // 延时 1s } }在函数中,声明一个变量 temperature 用于存储读取到的温度值。然后,调用函数 DS18B20_Init() 初始化 DS18B20 温度传感器。使用 DS18B20_Read_Temperature() 函数读取温度值,并且将结果存储在 temperature 变量中。最后,将读取到的温度值存储在全局变量 current_temperature 中。3.4 液位传感器任务 /* 液位传感器任务 */ void Liquid_Level_Sensor_Task(void *pvParameters) { uint16_t adc_value; float voltage; ​ /* 初始化液位传感器 GPIO 口 */ HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12, GPIO_PIN_RESET); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_13, GPIO_PIN_SET); ​ while (1) { /* 读取 ADC 值并计算电压值 */ if (HAL_ADC_Start(&hadc1) == HAL_OK) { if (HAL_ADC_PollForConversion(&hadc1, 10) == HAL_OK) { adc_value = HAL_ADC_GetValue(&hadc1); voltage = adc_value * 3.3 / 4096.0; } } ​ /* 根据电压值计算液位高度 */ if (voltage < 0.5) { liquid_level = 0.0; } else if (voltage > 2.5) { liquid_level = 100.0; } else { liquid_level = (voltage - 0.5) * 100.0 / 2.0; } ​ vTaskDelay(pdMS_TO_TICKS(1000)); // 延时 1s } }在函数中,声明变量 adc_value 和 voltage,分别用于存储读取到的 ADC 值和计算得到的电压值。使用 HAL_GPIO_WritePin() 函数初始化液位传感器 GPIO 口,将启用传感器的引脚设置为高电平。使用 if 条件语句检查 ADC 是否成功启动,并且使用 HAL_ADC_PollForConversion() 函数判断当前转换是否完成,如果转换完成,就获取 ADC 值,并且通过简单的计算公式将 ADC 值转换为电压值,并将结果存储在 voltage 变量中。由于需要使用电压值计算液位高度,使用 if 条件语句检查电压是否小于低液位警戒电压 0.5V 或者大于高液位警戒电压 2.5V,如果是则分别将液位高度设置为 0% 或 100%,否则使用简单的线性关系计算液位高度。3.5 控制任务 /* 控制任务 */ void Control_Task(void *pvParameters) { float temperature_setpoint = 25.0; // 设定温度值 float liquid_level_setpoint = 50.0; // 设定液位高度值 float temperature_error, liquid_level_error; float temperature_integral, liquid_level_integral; float temperature_derivative, liquid_level_derivative; float temperature_output, liquid_level_output; ​ float kp_temperature = 0.5, ki_temperature = 0.1, kd_temperature = 0.05; // 温度 PID 参数 float kp_liquid_level = 0.2, ki_liquid_level = 0.05, kd_liquid_level = 0.02; // 液位高度 PID 参数 ​ while (1) { /* 计算温度 PID 控制器输出 */ temperature_error = temperature_setpoint - current_temperature; temperature_integral += temperature_error; temperature_derivative = temperature_error - last_temperature_error; temperature_output = kp_temperature * temperature_error + ki_temperature * temperature_integral + kd_temperature * temperature_derivative; last_temperature_error = temperature_error; ​ /* 计算液位高度 PID 控制器输出 */ liquid_level_error = liquid_level_setpoint - liquid_level; liquid_level_integral += liquid_level_error; liquid_level_derivative = liquid_level_error - last_liquid_level_error; liquid_level_output = kp_liquid_level * liquid_level_error + ki_liquid_level * liquid_level_integral + kd_liquid_level * liquid_level_derivative; last_liquid_level_error = liquid_level_error; ​ /* 通过 PWM 控制加热器和水泵电机 */ if (temperature_output > 0.0) { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1, GPIO_PIN_SET); __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_2, (uint16_t)(temperature_output * 1000)); } else { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1, GPIO_PIN_RESET); __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_2, 0); } ​ if (liquid_level_output > 0.0) { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_2, GPIO_PIN_SET); __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, (uint16_t)(liquid_level_output * 1000)); } else { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_2, GPIO_PIN_RESET); __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, 0); } ​ vTaskDelay(pdMS_TO_TICKS(10)); // 延时 10ms } }在函数中:(1)定义参数和变量,包括设定温度值、设定液位高度值、温度 PID 控制器的参数、液位高度 PID 控制器的参数等。使用 while 循环处理控制逻辑,循环开始时,计算温度 PID 控制器输出。(2)计算当前误差,并将误差累积到积分项中。计算误差变化率,并使用 PID 参数计算出输出值,将结果存储在 temperature_output 中,并将当前误差存储在 last_temperature_error 中以便于下一次计算,计算液位高度 PID 控制器输出。(3)根据控制器输出值通过 PWM 控制加热器和水泵电机的运行状态。如果输出值大于 0,则启用电机或加热器并设置对应的 PWM 占空比,否则关闭电机或加热器并将 PWM 占空比设为 0。
  • [技术干货] 基于STM32设计的数显热水器
    一、项目介绍当前介绍的项目是基于 STM32F103ZET6 系列 MCU 设计的数显热水器,通过显示屏来显示热水器的温度及其工作状态,通过 PT100 传感器来检测热水器的温度变化,并通过电加热片实现加热过程,以达到控制热水器温度的目的。二、设计流程2.1 硬件选型STM32F103ZET6 系列 MCUOLED 显示屏PT100 温度传感器电加热片继电器2.2 软件设计(1)显示屏使用 OLED 显示屏来显示热水器的温度及其工作状态,通过 SPI 接口与 STM32 芯片进行通讯。设计温度值及其单位、热水器工作状态等。(2)温度传感器使用 PT100 温度传感器来检测热水器内部温度的变化,并将数据通过 ADC 转换后,传输给 STM32 芯片,以实现对热水器加热过程的控制。(3)电加热片使用电加热片模拟热水器加热过程,通过继电器控制电加热片的通断,以调节热水器的温度。(4)控制系统通过 STM32 芯片来实现对热水器的控制,读取温度传感器的数据。三、代码设计3.1 OLED显示屏(1)SPI 接口初始化需要对 STM32F103ZET6 的 SPI 接口进行初始化配置,设置相关的时钟和模式,使其能够与 OLED 显示屏进行通讯。 RCC_APB1PeriphClockCmd(RCC_APB1Periph_SPI3, ENABLE); // 打开SPI3时钟 SPI_InitTypeDef spi_init_type; spi_init_type.SPI_Direction = SPI_Direction_2Lines_FullDuplex; spi_init_type.SPI_Mode = SPI_Mode_Master; spi_init_type.SPI_DataSize = SPI_DataSize_8b; spi_init_type.SPI_CPOL = SPI_CPOL_Low; spi_init_type.SPI_CPHA = SPI_CPHA_1Edge; spi_init_type.SPI_NSS = SPI_NSS_Soft; spi_init_type.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_32; // 设置 SPI 时钟频率为 72 MHz / 32 = 2.25MHz spi_init_type.SPI_FirstBit = SPI_FirstBit_MSB; SPI_Init(SPI3, &spi_init_type); SPI_Cmd(SPI3, ENABLE);(2)OLED 显示屏初始化以下是 OLED 显示屏的初始化代码: void OLED_Init(void) { GPIO_SetBits(GPIOB, GPIO_Pin_6); //RST SET GPIO_ResetBits(GPIOB, GPIO_Pin_6); //RST RESET GPIO_SetBits(GPIOB, GPIO_Pin_6); //RST SET ​ write_command(0xAE); // 关闭显示 write_command(0xD5); // 设置时钟分频因子,震荡频率 write_command(0x80); // 分频因子=1 ,震荡频率(fosc)=8MHz write_command(0xA8); // 设置驱动路数:MUX(复用方式) write_command(0x1F); // 1/32 duty (0x0F~0x3F) write_command(0xD3); // 设置显示偏移 write_command(0x00); // 不偏移 write_command(0x40); // 设置显示开始行[5:0], 对于设置了32行的液晶, // 这里的值为0表示从0行开始显示 write_command(0x8D); // 对比度设置 write_command(0x14); // AHB参考电压256等分 移位[3:0]100[n,1/256] write_command(0x20); // 水平方向上的寻址模式 write_command(0x00); // 垂直方向上的寻址模式 write_command(0xA1); // 设置段再映射 write_command(0xC0); // 设置COM扫描方向 write_command(0xDA); // 设置COM引脚硬件配置 write_command(0x12); write_command(0x81); // 对比度设置 write_command(0xBF); // 设置电荷泵电压 write_command(0xD9); // 设置预充电周期 write_command(0xF1); write_command(0xDB); // 设置VCOMH电压倍率 write_command(0x40); write_command(0xAF); // 打开显示 ​ OLED_Clear(); // 清屏 }(3)OLED 显示函数接下来编写 OLED 显示函数,实现字符和数字的显示功能。 void OLED_show_string(uint8_t x, uint8_t y, char *str) { uint8_t i = 0; while (str[i] != '\0') { OLED_show_char(x, y + i * 8, str[i]); ++i; } } ​ void OLED_show_char(uint8_t x, uint8_t y, char ch) { uint8_t c = ch - 32; if (c >= 96) return; uint8_t* buffer = (uint8_t*)oled_buffer; uint8_t cx, cy; for (cy = 0; cy < 8; cy++) { uint8_t line = font[c][cy]; for (cx = 0; cx < 6; cx++) { if (line & 0x1) { buffer[(y + cy) * OLEDWIDTH + x + cx] = 1; } else { buffer[(y + cy) * OLEDWIDTH + x + cx] = 0; } line >>= 1; } } OLED_Draw_Pixel(x + 6, y, 0); OLED_Draw_Pixel(x + 6, y + 1, 0); OLED_Draw_Pixel(x + 6, y + 6, 0); OLED_Draw_Pixel(x + 6, y + 7, 0); }(4)结果显示在代码中调用 OLED_show_string 函数和 OLED_show_char 函数显示数值和字符。 OLED_Init(); OLED_Clear(); OLED_show_string(0, 0, "HELLO WORLD!"); OLED_show_string(0, 16, "TEMP:20 C");3.2 测温代码(1)引脚配置需要对 STM32F103ZET6 的 IO 口进行配置,将用于连接 PT100 温度传感器的引脚设置为输入模式。这里以 PA0 引脚作为 PT100 传感器的连接口(即 PT100 三线连接中的 R3 端),代码如下: GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; // 上拉输入模式 GPIO_Init(GPIOA, &GPIO_InitStructure);(2)ADC 配置接下来需要对 STM32F103ZET6 的 ADC 进行初始化配置,使其能够读取 PT100 温度传感器输出的电压信号。这里以 ADC1 通道5 作为读取口,代码如下: ADC_InitTypeDef ADC_InitStructure; RCC_ADCCLKConfig(RCC_PCLK2_Div6); // 设置 ADC 时钟为 PCLK2 的 1/6 RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE); // 打开 ADC1 时钟 ADC_DeInit(ADC1); // 初始化 ADC1 ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; ADC_InitStructure.ADC_ScanConvMode = DISABLE; ADC_InitStructure.ADC_ContinuousConvMode = ENABLE; // 连续转换模式 ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; ADC_InitStructure.ADC_NbrOfChannel = 1; ADC_Init(ADC1, &ADC_InitStructure); ADC_Cmd(ADC1, ENABLE); // 开启 ADC1(3)温度转换函数根据 PT100 温度传感器输出电压与温度的关系,可使用线性函数计算出温度值。转换公式如下: Rt = (Vref - Vpt) / Ipt // Rt 为 PT100 的阻值,Vref 为基准电压,Vpt 为 PT100 输出电压,Ipt 为 PT100 驱动电流 Temp = a * Rt + b // Temp 为温度值,a 和 b 为经过拟合后的系数其中 Rt 的计算需要使用差分运算放大器进行转换,这里不再赘述。假设已经得到 Rt 值,则温度转换函数代码如下: float PT100_Get_Temperature(float Rt) { float a = 3.9083e-3f, b = -5.775e-7f, R0 = 100.0f; // 根据实际数据进行拟合得到 a、b 和 R0 的值 float Tem, delta; delta = pow(Rt / R0, 2) + a * (Rt / R0) + b; Tem = (delta > 0) ? (-R0*a + sqrt(delta)) / (2 * b) : 0; return Tem; }(4)数据采集根据差分放大器输出的电压值得到 PT100 温度传感器的阻值,再根据阻值计算出实际温度,最后将温度值通过串口打印出来。以下是数据采集代码:float ADC_Get_Voltage(void){ float voltage = 0; uint16_t adc_val = 0; ADC_RegularChannelConfig(ADC1, ADC_Channel_5, 1, ADC_SampleTime_239Cycles5); // 配置 ADC 通道5 ADC_SoftwareStartConvCmd(ADC1, ENABLE); // 使能软件触发 ADC 转换 while (!ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC)); // 等待转换结束 adc_val = ADC_GetConversionValue(ADC1); // 读取 ADC 转换结果 voltage = (float)adc_val * 3.3f / 4096; // 计算基准电压 return voltage;}float PT100_Get_Rt(float Vpt){ float Rsource = 10e3f, Rpt = 100.0f; // Rsource 为差分放大器输出电阻,Rpt 为 PT100 阻值 float Ipt = (3.3f - Vpt) / Rsource; // 计算 PT100 驱动电流 float Rt = (3.3f - Vpt) / Ipt; // 根据欧姆定律计算出 PT100 阻值 return Rt;}void USART1_Send_Float(float f){ char buf[32]; sprintf(buf, "%.1f\r\n", f); // 转换为字符串 while (*buf) { USART_SendData(USART1, *buf); while (USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET); buf++; }}int main(void){ ... while (1) { float Vpt = ADC_Get_Voltage(); // 获取差分放大器输出电压 float Rt = PT100_Get_Rt(Vpt); // 计算 PT100 阻值 float Temp = PT100_Get_Temperature(Rt); // 根据阻值计算温度 USART1_Send_Float(Temp); // 将温度值打印到串口 delay_ms(500); } ...}
  • [技术干货] 基于STM32设计的数码相册
    一、项目介绍项目是基于STM32设计的数码相册,能够通过LCD显示屏解码显示主流的图片,支持bmp、jpg、gif等格式。用户可以通过按键或者触摸屏来切换图片,同时还可以旋转显示,并能够自适应居中显示,小尺寸图片居中显示,大尺寸图片自动缩小显示(超出屏幕范围)。图片从SD卡中获取。二、设计思路2.1 硬件设计本项目所需的主要硬件:STM32F103ZET6LCD屏幕SD卡模块按键和触摸屏2.2 软件设计(1)解码图片在STM32芯片中,解码图片需要将读取到的数据存入图形缓冲区中,以便进行图画显示。常用的解码算法有JPEG解码和BMP解码。(2)图片显示为了更好的实现图片旋转和缩放功能,在显示图片时需对其进行矩阵运算。通过左右翻转和上下翻转,可实现图片的旋转功能。通过计算图片与显示屏幕之间的比例关系并进行缩放,实现自适应居中和图片的缩放功能。(3)SD卡SD卡模块可通过SPI接口与STM32芯片进行通信,读取SD卡中的图片数据,实现对图片的加载和显示。(4)按键和触摸屏在使用过程中,用户可以通过按键和触摸屏对图片进行切换、旋转和缩放等操作。通过设置中断处理函数,响应用户的操作并及时更新显示屏幕上的图片。2.3 图片播放流程图2.4 显示效果三、代码设计3.1 主函数 #include "stm32f10x.h" #include "led.h" #include "delay.h" #include "key.h" #include "usart.h" #include <string.h> #include <stdio.h> #include "sd.h" //SD卡 #include "ff.h" //文件系统 #include "bmp.h" //文件系统 #include "iic.h" #include "at24c02.h" #include "xpt2046.h" #include "lcd.h" ​ ​ FATFS fs; // 用户定义的文件系统结构体 int main() { DIR dir_dp; FILINFO file_info; u32 sd_size; //存放SD卡返回的容量 BeepInit(); //蜂鸣器初始化 LedInit(); //LED灯初始化 UsartInit(USART1,72,115200); KeyInit(); //按键初始化 IICInit(); LcdInit(); TOUCH_Init(); //TOUCH_ADJUST(); //触摸屏校准 printf("串口工作正常!\r\n"); if(SDCardDeviceInit()) { printf("SD卡初始化失败!\r\n"); } sd_size=GetSDCardSectorCount(); //检测SD卡大小,返回值右移11位得到以M为单位的容量 printf("SD卡Sizeof:%d\r\n",sd_size>>11); f_mount(&fs,"0:",1); // 注册文件系统工作区,驱动器号 0,初始化后其他函数可使用里面的参数 LcdClear(0xFFFF); //f_mkdir("0:/目录创建测试!"); //测试OK //f_unlink("0:/123"); //删除目录,注意只能删除空目录 //f_unlink("0:/1.bmp");//删除文件 //printf("%d\r\n",Show_BMP("1.bmp")); if(f_opendir(&dir_dp,"0:/bmp")!=FR_OK)printf("目录打开失败!\r\n"); //循环读取目录 while(f_readdir(&dir_dp,&file_info)==FR_OK) { if(file_info.fname[0]==0)break; //判断目录跳出条件,表示目录已经读取完毕 if(strstr(file_info.fname,".bmp")) //过滤目录 { printf("文件名称: %s,文件大小: %ld 字节\r\n",file_info.fname,file_info.fsize); }else printf("文件名称: %s,文件大小: %ld 字节\r\n",file_info.fname,file_info.fsize); } if(f_closedir(&dir_dp)!=FR_OK)printf("目录关闭失败!\r\n"); while(1) { LED1=!LED1; DelayMs(100); } } ​3.2 BMP图片解码 #include "bmp.h" unsigned short RGB888ToRGB565(unsigned int n888Color) { unsigned short n565Color = 0; // 获取RGB单色,并截取高位 unsigned char cRed = (n888Color & RGB888_RED) >> 19; unsigned char cGreen = (n888Color & RGB888_GREEN) >> 10; unsigned char cBlue = (n888Color & RGB888_BLUE) >> 3; // 连接 n565Color = (cRed << 11) + (cGreen << 5) + (cBlue << 0); return n565Color; } ​ unsigned int RGB565ToRGB888(unsigned short n565Color) { unsigned int n888Color = 0; // 获取RGB单色,并填充低位 unsigned char cRed = (n565Color & RGB565_RED) >> 8; unsigned char cGreen = (n565Color & RGB565_GREEN) >> 3; unsigned char cBlue = (n565Color & RGB565_BLUE) << 3; // 连接 n888Color = (cRed << 16) + (cGreen << 8) + (cBlue << 0); return n888Color; } ​ ​ ​ /* 函数功能:实现截图功能 参 数: char filename:文件名称 返 回 值:0表示成功,1表示失败 */ u8 C_BMP(const char *filename,u32 Width,u32 Height) { FIL file; // 用户定义的文件系统结构体 u8 res; // 保存文件操作的返回值 BITMAPFILEHEADER BmpHead; //保存图片文件头的信息 BITMAPINFOHEADER BmpInfo; //图片参数信息 char *p; u32 cnt,c_32; int x,y; u16 c_16; //存放16位的颜色 /*1. 创建一张BMP图片*/ res = f_open(&file,filename, FA_OPEN_ALWAYS | FA_WRITE); if(res!=0)return 1; /*2. 创建BMP的图片头参数*/ memset(&BmpHead,0,sizeof(BITMAPFILEHEADER)); //将指定空间赋值为指定的值 p=(char*)&BmpHead.bfType; //填充BMP图片的类型 *p='B'; *(p+1)='M'; ​ //BmpHead.bfType=0x4d42;//'B''M' //0x4d42 BmpHead.bfSize=Width*Height*3+54; //图片的总大小 BmpHead.bfOffBits=54; //图片数据的偏移量 res =f_write(&file,&BmpHead,sizeof(BITMAPFILEHEADER),&cnt); if(res!=0)return 1; /*3. 创建BMP图片的参数*/ memset(&BmpInfo,0,sizeof(BITMAPINFOHEADER)); BmpInfo.biSize=sizeof(BITMAPINFOHEADER); //当前结构体大小 BmpInfo.biWidth=Width; BmpInfo.biHeight=Height; BmpInfo.biPlanes=1; BmpInfo.biBitCount=24; res =f_write(&file,&BmpInfo,sizeof(BITMAPINFOHEADER),&cnt); if(res!=0)return 1; /*4. 读取LCD屏的颜色数据,用于创建BMP图片*/ for(y=Height-1;y>=0;y--) { for(x=0;x<Width;x++) { c_16=LcdReadPoint(x,y); //读取LCD屏上一个点的颜色 c_32=RGB565ToRGB888(c_16); //颜色的转换 res =f_write(&file,&c_32,3,&cnt); if(res!=0)return 1; } } /*5. 关闭文件*/ f_close(&file); } ​ ​ /* 函数功能:BMP图片显示功能 参 数: char filename:文件名称 返 回 值:0表示成功,1表示失败 */ u8 Show_BMP(const char *filename) { FIL file; // 用户定义的文件系统结构体 u8 res; // 保存文件操作的返回值 BITMAPFILEHEADER BmpHead; //保存图片文件头的信息 BITMAPINFOHEADER BmpInfo; //图片参数信息 char *p; u32 cnt,c_24; int x,y; u16 c_16; //存放16位的颜色 /*1. 打开一张BMP图片*/ res = f_open(&file,filename,FA_READ); if(res!=0)return 1; /*2. 读取BMP的图片头参数*/ res =f_read(&file,&BmpHead,sizeof(BITMAPFILEHEADER),&cnt); if(res!=0)return 1; /*3. 读取BMP图片的参数*/ res =f_read(&file,&BmpInfo,sizeof(BITMAPINFOHEADER),&cnt); if(res!=0)return 1; /*4.显示BMP图片*/ f_lseek(&file,BmpHead.bfOffBits); //移动到RGB数据的存放位置 //后期的优化:读取一行的数据,再显示一行。 for(y=0;y<BmpInfo.biHeight;y++) { for(x=0;x<BmpInfo.biWidth;x++) { res =f_read(&file,&c_24,3,&cnt); if(res!=0)return 1; c_16=RGB888ToRGB565(c_24); //转换颜色 LcdDrawPoint(x,y,c_16); } } /*5. 关闭文件*/ f_close(&file); } ​ ​3.3 jpeg图片解码 #include "piclib.h" #include "nt35310_lcd.h" _pic_info picinfo; //图片信息 _pic_phy pic_phy; //图片显示物理接口 ​ /* 函数功能: 划横线函数,需要自己实现 */ void Picture_DrawLine(u16 x0,u16 y0,u16 len,u16 color) { NT35310_Fill(x0,y0,x0+len-1,y0,color); } ​ /* 函数功能: 矩形填充颜色 函数参数: x,y:起始坐标 width,height:宽度和高度。 color:颜色数组 */ void Picture_FillColor(u16 x,u16 y,u16 width,u16 height,u16 *color) { NT35310_DrawRectangle(x,y,x+width-1,y+height-1,*color); } ​ /* 函数功能: 画图初始化,在画图之前,必须先调用此函数 函数参数: 指定画点/读点 */ void Picture_DisplayInit(void) { pic_phy.draw_point=NT35310_DrawPoint; //画点函数实现 pic_phy.fill=NT35310_Fill; //填充函数实现,仅GIF需要 pic_phy.draw_hline=Picture_DrawLine; //画线函数实现,仅GIF需要 pic_phy.fillcolor=Picture_FillColor; //颜色填充函数实现,仅TJPGD需要 picinfo.lcdwidth=Lcd_Width; //得到LCD的宽度像素 picinfo.lcdheight=Lcd_Height; //得到LCD的高度像素 ​ picinfo.ImgWidth=0; //初始化宽度为0 picinfo.ImgHeight=0;//初始化高度为0 picinfo.Div_Fac=0; //初始化缩放系数为0 picinfo.S_Height=0; //初始化设定的高度为0 picinfo.S_Width=0; //初始化设定的宽度为0 picinfo.S_XOFF=0; //初始化x轴的偏移量为0 picinfo.S_YOFF=0; //初始化y轴的偏移量为0 picinfo.staticx=0; //初始化当前显示到的x坐标为0 picinfo.staticy=0; //初始化当前显示到的y坐标为0 } ​ ​ /* 函数功能: 初始化智能画点 说明: 内部调用 */ void Picture_PointInit(void) { float temp,temp1; temp=(float)picinfo.S_Width/picinfo.ImgWidth; temp1=(float)picinfo.S_Height/picinfo.ImgHeight; if(temp<temp1)temp1=temp;//取较小的那个 if(temp1>1)temp1=1; //使图片处于所给区域的中间 picinfo.S_XOFF+=(picinfo.S_Width-temp1*picinfo.ImgWidth)/2; picinfo.S_YOFF+=(picinfo.S_Height-temp1*picinfo.ImgHeight)/2; temp1*=8192;//扩大8192倍 picinfo.Div_Fac=temp1; picinfo.staticx=0xffff; picinfo.staticy=0xffff;//放到一个不可能的值上面 } ​ /* 函数功能: 判断这个像素是否可以显示 函数参数: (x,y) :像素原始坐标 chg :功能变量. 返回值:0,不需要显示.1,需要显示 */ u8 Picture_is_Pixel(u16 x,u16 y,u8 chg) { if(x!=picinfo.staticx||y!=picinfo.staticy) { if(chg==1) { picinfo.staticx=x; picinfo.staticy=y; } return 1; }else return 0; } ​ extern u8 jpg_decode(const u8 *filename); ​ /* 函数功能: 绘制图片 函数参数: FileName:要显示的图片文件 BMP/JPG/JPEG/GIF x,y,width,height:坐标及显示区域尺寸 fast:使能jpeg/jpg小图片(图片尺寸小于等于液晶分辨率)快速解码,0,不使能;1,使能. 函数说明: 图片在开始和结束的坐标点范围内显示 */ u8 Picture_DisplayJPG(const u8 *filename,u16 x,u16 y,u16 width,u16 height,u8 fast) { u8 res;//返回值 //显示的图片高度、宽度 picinfo.S_Height=height; picinfo.S_Width=width; ​ //显示的开始坐标点 picinfo.S_YOFF=y; picinfo.S_XOFF=x; //解码JPG/JPEG res=jpg_decode(filename); //解码JPG/JPEG return res; } ​3.4 gif图片解码 #include "piclib.h" #include <stm32f10x.h> #include "gif.h" #include "ff.h" #include "delay.h" #include <string.h> ​ const u16 _aMaskTbl[16] = { 0x0000, 0x0001, 0x0003, 0x0007, 0x000f, 0x001f, 0x003f, 0x007f, 0x00ff, 0x01ff, 0x03ff, 0x07ff, 0x0fff, 0x1fff, 0x3fff, 0x7fff, }; const u8 _aInterlaceOffset[]={8,8,4,2}; const u8 _aInterlaceYPos []={0,4,2,1}; u8 gifdecoding=0;//标记GIF正在解码. ​ //检测GIF头 //返回值:0,是GIF89a/87a;非零,非GIF89a/87a u8 gif_check_head(FIL *file) { u8 gifversion[6]; u32 readed; u8 res; res=f_read(file,gifversion,6,(UINT*)&readed); if(res)return 1; if((gifversion[0]!='G')||(gifversion[1]!='I')||(gifversion[2]!='F')|| (gifversion[3]!='8')||((gifversion[4]!='7')&&(gifversion[4]!='9'))|| (gifversion[5]!='a'))return 2; else return 0; } ​ //将RGB888转为RGB565 //ctb:RGB888颜色数组首地址. //返回值:RGB565颜色. u16 gif_getrgb565(u8 *ctb) { u16 r,g,b; r=(ctb[0]>>3)&0X1F; g=(ctb[1]>>2)&0X3F; b=(ctb[2]>>3)&0X1F; return b+(g<<5)+(r<<11); } ​ //读取颜色表 //file:文件; //gif:gif信息; //num:tbl大小. //返回值:0,OK;其他,失败; u8 gif_readcolortbl(FIL *file,gif89a * gif,u16 num) { u8 rgb[3]; u16 t; u8 res; u32 readed; for(t=0;t<num;t++) { res=f_read(file,rgb,3,(UINT*)&readed); if(res)return 1;//读错误 gif->colortbl[t]=gif_getrgb565(rgb); } return 0; } //得到逻辑屏幕描述,图像尺寸等 //file:文件; //gif:gif信息; //返回值:0,OK;其他,失败; u8 gif_getinfo(FIL *file,gif89a * gif) { u32 readed; u8 res; res=f_read(file,(u8*)&gif->gifLSD,7,(UINT*)&readed); if(res)return 1; if(gif->gifLSD.flag&0x80)//存在全局颜色表 { gif->numcolors=2<<(gif->gifLSD.flag&0x07);//得到颜色表大小 if(gif_readcolortbl(file,gif,gif->numcolors))return 1;//读错误 } return 0; } //保存全局颜色表 //gif:gif信息; void gif_savegctbl(gif89a* gif) { u16 i=0; for(i=0;i<256;i++)gif->bkpcolortbl[i]=gif->colortbl[i];//保存全局颜色. } //恢复全局颜色表 //gif:gif信息; void gif_recovergctbl(gif89a* gif) { u16 i=0; for(i=0;i<256;i++)gif->colortbl[i]=gif->bkpcolortbl[i];//恢复全局颜色. } ​ //初始化LZW相关参数 //gif:gif信息; //codesize:lzw码长度 void gif_initlzw(gif89a* gif,u8 codesize) { memset((u8 *)gif->lzw, 0, sizeof(LZW_INFO)); gif->lzw->SetCodeSize = codesize; gif->lzw->CodeSize = codesize + 1; gif->lzw->ClearCode = (1 << codesize); gif->lzw->EndCode = (1 << codesize) + 1; gif->lzw->MaxCode = (1 << codesize) + 2; gif->lzw->MaxCodeSize = (1 << codesize) << 1; gif->lzw->ReturnClear = 1; gif->lzw->LastByte = 2; gif->lzw->sp = gif->lzw->aDecompBuffer; } ​ //读取一个数据块 //gfile:gif文件; //buf:数据缓存区 //maxnum:最大读写数据限制 u16 gif_getdatablock(FIL *gfile,u8 *buf,u16 maxnum) { u8 cnt; u32 readed; u32 fpos; f_read(gfile,&cnt,1,(UINT*)&readed);//得到LZW长度 if(cnt) { if (buf)//需要读取 { if(cnt>maxnum) { fpos=f_tell(gfile); f_lseek(gfile,fpos+cnt);//跳过 return cnt;//直接不读 } f_read(gfile,buf,cnt,(UINT*)&readed);//得到LZW长度 }else //直接跳过 { fpos=f_tell(gfile); f_lseek(gfile,fpos+cnt);//跳过 } } return cnt; } ​ //ReadExtension //Purpose: //Reads an extension block. One extension block can consist of several data blocks. //If an unknown extension block occures, the routine failes. //返回值:0,成功; // 其他,失败 u8 gif_readextension(FIL *gfile,gif89a* gif, int *pTransIndex,u8 *pDisposal) { u8 temp; u32 readed; u8 buf[4]; f_read(gfile,&temp,1,(UINT*)&readed);//得到长度 switch(temp) { case GIF_PLAINTEXT: case GIF_APPLICATION: case GIF_COMMENT: while(gif_getdatablock(gfile,0,256)>0); //获取数据块 return 0; case GIF_GRAPHICCTL://图形控制扩展块 if(gif_getdatablock(gfile,buf,4)!=4)return 1; //图形控制扩展块的长度必须为4 gif->delay=(buf[2]<<8)|buf[1]; //得到延时 *pDisposal=(buf[0]>>2)&0x7; //得到处理方法 if((buf[0]&0x1)!=0)*pTransIndex=buf[3]; //透明色表 f_read(gfile,&temp,1,(UINT*)&readed); //得到LZW长度 if(temp!=0)return 1; //读取数据块结束符错误. return 0; } return 1;//错误的数据 } ​ //从LZW缓存中得到下一个LZW码,每个码包含12位 //返回值:<0,错误. // 其他,正常. int gif_getnextcode(FIL *gfile,gif89a* gif) { int i,j,End; long Result; if(gif->lzw->ReturnClear) { //The first code should be a clearcode. gif->lzw->ReturnClear=0; return gif->lzw->ClearCode; } End=gif->lzw->CurBit+gif->lzw->CodeSize; if(End>=gif->lzw->LastBit) { int Count; if(gif->lzw->GetDone)return-1;//Error gif->lzw->aBuffer[0]=gif->lzw->aBuffer[gif->lzw->LastByte-2]; gif->lzw->aBuffer[1]=gif->lzw->aBuffer[gif->lzw->LastByte-1]; if((Count=gif_getdatablock(gfile,&gif->lzw->aBuffer[2],300))==0)gif->lzw->GetDone=1; if(Count<0)return -1;//Error gif->lzw->LastByte=2+Count; gif->lzw->CurBit=(gif->lzw->CurBit-gif->lzw->LastBit)+16; gif->lzw->LastBit=(2+Count)*8; End=gif->lzw->CurBit+gif->lzw->CodeSize; } j=End>>3; i=gif->lzw->CurBit>>3; if(i==j)Result=(long)gif->lzw->aBuffer[i]; else if(i+1==j)Result=(long)gif->lzw->aBuffer[i]|((long)gif->lzw->aBuffer[i+1]<<8); else Result=(long)gif->lzw->aBuffer[i]|((long)gif->lzw->aBuffer[i+1]<<8)|((long)gif->lzw->aBuffer[i+2]<<16); Result=(Result>>(gif->lzw->CurBit&0x7))&_aMaskTbl[gif->lzw->CodeSize]; gif->lzw->CurBit+=gif->lzw->CodeSize; return(int)Result; } ​ ​ //得到LZW的下一个码 //返回值:<0,错误(-1,不成功;-2,读到结束符了) // >=0,OK.(LZW的第一个码) int gif_getnextbyte(FIL *gfile,gif89a* gif) { int i,Code,Incode; while((Code=gif_getnextcode(gfile,gif))>=0) { if(Code==gif->lzw->ClearCode) { //Corrupt GIFs can make this happen if(gif->lzw->ClearCode>=(1<<MAX_NUM_LWZ_BITS))return -1;//Error //Clear the tables memset((u8*)gif->lzw->aCode,0,sizeof(gif->lzw->aCode)); for(i=0;i<gif->lzw->ClearCode;++i)gif->lzw->aPrefix[i]=i; //Calculate the'special codes' independence of the initial code size //and initialize the stack pointer gif->lzw->CodeSize=gif->lzw->SetCodeSize+1; gif->lzw->MaxCodeSize=gif->lzw->ClearCode<<1; gif->lzw->MaxCode=gif->lzw->ClearCode+2; gif->lzw->sp=gif->lzw->aDecompBuffer; //Read the first code from the stack after clear ingand initializing*/ do { gif->lzw->FirstCode=gif_getnextcode(gfile,gif); }while(gif->lzw->FirstCode==gif->lzw->ClearCode); gif->lzw->OldCode=gif->lzw->FirstCode; return gif->lzw->FirstCode; } if(Code==gif->lzw->EndCode)return -2;//End code Incode=Code; if(Code>=gif->lzw->MaxCode) { *(gif->lzw->sp)++=gif->lzw->FirstCode; Code=gif->lzw->OldCode; } while(Code>=gif->lzw->ClearCode) { *(gif->lzw->sp)++=gif->lzw->aPrefix[Code]; if(Code==gif->lzw->aCode[Code])return Code; if((gif->lzw->sp-gif->lzw->aDecompBuffer)>=sizeof(gif->lzw->aDecompBuffer))return Code; Code=gif->lzw->aCode[Code]; } *(gif->lzw->sp)++=gif->lzw->FirstCode=gif->lzw->aPrefix[Code]; if((Code=gif->lzw->MaxCode)<(1<<MAX_NUM_LWZ_BITS)) { gif->lzw->aCode[Code]=gif->lzw->OldCode; gif->lzw->aPrefix[Code]=gif->lzw->FirstCode; ++gif->lzw->MaxCode; if((gif->lzw->MaxCode>=gif->lzw->MaxCodeSize)&&(gif->lzw->MaxCodeSize<(1<<MAX_NUM_LWZ_BITS))) { gif->lzw->MaxCodeSize<<=1; ++gif->lzw->CodeSize; } } gif->lzw->OldCode=Incode; if(gif->lzw->sp>gif->lzw->aDecompBuffer)return *--(gif->lzw->sp); } return Code; } //DispGIFImage //Purpose: // This routine draws a GIF image from the current pointer which should point to a // valid GIF data block. The size of the desired image is given in the image descriptor. //Return value: // 0 if succeed // 1 if not succeed //Parameters: // pDescriptor - Points to a IMAGE_DESCRIPTOR structure, which contains infos about size, colors and interlacing. // x0, y0 - Obvious. // Transparency - Color index which should be treated as transparent. // Disposal - Contains the disposal method of the previous image. If Disposal == 2, the transparent pixels // of the image are rendered with the background color. u8 gif_dispimage(FIL *gfile,gif89a* gif,u16 x0,u16 y0,int Transparency, u8 Disposal) { u32 readed; u8 lzwlen; int Index,OldIndex,XPos,YPos,YCnt,Pass,Interlace,XEnd; int Width,Height,Cnt,ColorIndex; u16 bkcolor; u16 *pTrans; ​ Width=gif->gifISD.width; Height=gif->gifISD.height; XEnd=Width+x0-1; bkcolor=gif->colortbl[gif->gifLSD.bkcindex]; pTrans=(u16*)gif->colortbl; f_read(gfile,&lzwlen,1,(UINT*)&readed);//得到LZW长度 gif_initlzw(gif,lzwlen);//Initialize the LZW stack with the LZW code size Interlace=gif->gifISD.flag&0x40;//是否交织编码 for(YCnt=0,YPos=y0,Pass=0;YCnt<Height;YCnt++) { Cnt=0; OldIndex=-1; for(XPos=x0;XPos<=XEnd;XPos++) { if(gif->lzw->sp>gif->lzw->aDecompBuffer)Index=*--(gif->lzw->sp); else Index=gif_getnextbyte(gfile,gif); if(Index==-2)return 0;//Endcode if((Index<0)||(Index>=gif->numcolors)) { //IfIndex out of legal range stop decompressing return 1;//Error } //If current index equals old index increment counter if((Index==OldIndex)&&(XPos<=XEnd))Cnt++; else { if(Cnt) { if(OldIndex!=Transparency) { pic_phy.draw_hline(XPos-Cnt-1,YPos,Cnt+1,*(pTrans+OldIndex)); }else if(Disposal==2) { pic_phy.draw_hline(XPos-Cnt-1,YPos,Cnt+1,bkcolor); } Cnt=0; }else { if(OldIndex>=0) { if(OldIndex!=Transparency)pic_phy.draw_point(XPos-1,YPos,*(pTrans+OldIndex)); else if(Disposal==2)pic_phy.draw_point(XPos-1,YPos,bkcolor); } } } OldIndex=Index; } if((OldIndex!=Transparency)||(Disposal==2)) { if(OldIndex!=Transparency)ColorIndex=*(pTrans+OldIndex); else ColorIndex=bkcolor; if(Cnt) { pic_phy.draw_hline(XPos-Cnt-1,YPos,Cnt+1,ColorIndex); }else pic_phy.draw_point(XEnd,YPos,ColorIndex); } //Adjust YPos if image is interlaced if(Interlace)//交织编码 { YPos+=_aInterlaceOffset[Pass]; if((YPos-y0)>=Height) { ++Pass; YPos=_aInterlaceYPos[Pass]+y0; } }else YPos++; } return 0; } ​ ​ /* 函数功能: 恢复成背景色 函数参数: x,y:坐标 gif:gif信息. pimge:图像描述块信息 */ void gif_clear2bkcolor(u16 x,u16 y,gif89a* gif,ImageScreenDescriptor pimge) { u16 x0,y0,x1,y1; u16 color=gif->colortbl[gif->gifLSD.bkcindex]; if(pimge.width==0||pimge.height==0)return;//直接不用清除了,原来没有图像!! if(gif->gifISD.yoff>pimge.yoff) { x0=x+pimge.xoff; y0=y+pimge.yoff; x1=x+pimge.xoff+pimge.width-1;; y1=y+gif->gifISD.yoff-1; if(x0<x1&&y0<y1&&x1<320&&y1<320)pic_phy.fill(x0,y0,x1,y1,color); //设定xy,的范围不能太大. } if(gif->gifISD.xoff>pimge.xoff) { x0=x+pimge.xoff; y0=y+pimge.yoff; x1=x+gif->gifISD.xoff-1;; y1=y+pimge.yoff+pimge.height-1; if(x0<x1&&y0<y1&&x1<320&&y1<320)pic_phy.fill(x0,y0,x1,y1,color); } if((gif->gifISD.yoff+gif->gifISD.height)<(pimge.yoff+pimge.height)) { x0=x+pimge.xoff; y0=y+gif->gifISD.yoff+gif->gifISD.height-1; x1=x+pimge.xoff+pimge.width-1;; y1=y+pimge.yoff+pimge.height-1; if(x0<x1&&y0<y1&&x1<320&&y1<320)pic_phy.fill(x0,y0,x1,y1,color); } if((gif->gifISD.xoff+gif->gifISD.width)<(pimge.xoff+pimge.width)) { x0=x+gif->gifISD.xoff+gif->gifISD.width-1; y0=y+pimge.yoff; x1=x+pimge.xoff+pimge.width-1;; y1=y+pimge.yoff+pimge.height-1; if(x0<x1&&y0<y1&&x1<320&&y1<320)pic_phy.fill(x0,y0,x1,y1,color); } } ​ ​ /* 函数功能: 画GIF图像的一帧 函数参数: gfile:gif文件 x0,y0:开始显示的坐标 */ u8 gif_drawimage(FIL *gfile,gif89a* gif,u16 x0,u16 y0) { u32 readed; u8 res,temp; u16 numcolors; ImageScreenDescriptor previmg; ​ u8 Disposal; int TransIndex; u8 Introducer; TransIndex=-1; do { res=f_read(gfile,&Introducer,1,(UINT*)&readed);//读取一个字节 if(res)return 1; switch(Introducer) { case GIF_INTRO_IMAGE://图像描述 previmg.xoff=gif->gifISD.xoff; previmg.yoff=gif->gifISD.yoff; previmg.width=gif->gifISD.width; previmg.height=gif->gifISD.height; ​ res=f_read(gfile,(u8*)&gif->gifISD,9,(UINT*)&readed);//读取一个字节 if(res)return 1; if(gif->gifISD.flag&0x80)//存在局部颜色表 { gif_savegctbl(gif);//保存全局颜色表 numcolors=2<<(gif->gifISD.flag&0X07);//得到局部颜色表大小 if(gif_readcolortbl(gfile,gif,numcolors))return 1;//读错误 } if(Disposal==2)gif_clear2bkcolor(x0,y0,gif,previmg); gif_dispimage(gfile,gif,x0+gif->gifISD.xoff,y0+gif->gifISD.yoff,TransIndex,Disposal); while(1) { f_read(gfile,&temp,1,(UINT*)&readed);//读取一个字节 if(temp==0)break; readed=f_tell(gfile);//还存在块. if(f_lseek(gfile,readed+temp))break;//继续向后偏移 } if(temp!=0)return 1;//Error return 0; case GIF_INTRO_TERMINATOR://得到结束符了 return 2; //代表图像解码完成了. case GIF_INTRO_EXTENSION: res=gif_readextension(gfile,gif,&TransIndex,&Disposal);//读取图像扩展块消息 if(res)return 1; break; default: return 1; } }while(Introducer!=GIF_INTRO_TERMINATOR);//读到结束符了 return 0; } ​ ​ /* 函数功能: 退出当前解码 */ void gif_quit(void) { gifdecoding=0; } ​ ​ /* 函数功能: 解码一个gif文件 函数参数: filename:带路径的gif文件名字 x,y,width,height:显示坐标及区域大小. */ u8 gif_decode(const u8 *filename,u16 x,u16 y,u16 width,u16 height) { u8 res=0; u16 dtime=0;//解码延时 gif89a *mygif89a; FIL *gfile; gfile=(FIL*)SRAM_Malloc(sizeof(FIL)); if(gfile==NULL)res=PIC_MEM_ERR;//申请内存失败 mygif89a=(gif89a*)SRAM_Malloc(sizeof(gif89a)); if(mygif89a==NULL)res=PIC_MEM_ERR;//申请内存失败 mygif89a->lzw=(LZW_INFO*)SRAM_Malloc(sizeof(LZW_INFO)); if(mygif89a->lzw==NULL)res=PIC_MEM_ERR;//申请内存失败 if(res==0)//OK { res=f_open(gfile,(TCHAR *)filename,FA_READ); if(res==0)//打开文件ok { if(gif_check_head(gfile))res=PIC_FORMAT_ERR; if(gif_getinfo(gfile,mygif89a))res=PIC_FORMAT_ERR; if(mygif89a->gifLSD.width>width||mygif89a->gifLSD.height>height)res=PIC_SIZE_ERR;//尺寸太大. else { x=(width-mygif89a->gifLSD.width)/2+x; y=(height-mygif89a->gifLSD.height)/2+y; } gifdecoding=1; while(gifdecoding&&res==0)//解码循环 { res=gif_drawimage(gfile,mygif89a,x,y);//显示一张图片 if(mygif89a->gifISD.flag&0x80)gif_recovergctbl(mygif89a);//恢复全局颜色表 if(mygif89a->delay)dtime=mygif89a->delay; else dtime=10;//默认延时 while(dtime--&&gifdecoding)delay_ms(10);//延迟 if(res==2) { res=0; break; } } } f_close(gfile); } SRAM_Free(gfile); SRAM_Free(mygif89a->lzw); SRAM_Free(mygif89a); return res; } ​
  • [技术干货] 基于STM32设计的人体健康检测仪
    一、项目介绍当前文章介绍基于STM32设计的人体健康检测仪。设备采用STM32系列MCU作为主控芯片,配备血氧浓度传感器(使用MAX30102血氧浓度检测传感器)、OLED屏幕和电池供电等外设模块。设备可以广泛应用于医疗、健康等领域。可以帮助医生和病人更好地了解病情变化,提高治疗效果和生活质量。设备也可以用于健康管理、运动监测等场景,帮助用户了解自己的身体状况,保持健康的生活方式。在项目中,使用了KEIL作为开发平台和工具,通过血氧模块采集人体的心跳和血氧浓度参数,并通过OLED屏幕显示现在的心跳和血氧浓度。同时,通过指标分析,提供采集到的数据与正常指标比对,分析被检测人员的健康状态。采集的数据可通过蓝牙或者WIFI传递给手机APP进行处理,方便用户随时了解自己的身体状况。本设计采用STM32为主控芯片,搭配血氧浓度传感器和OLED屏幕,实现了人体健康数据的采集和展示,并对采集到的数据进行分析,判断被检测人员的健康状态。同时,设计使用蓝牙或WiFi将采集到的数据传递给手机APP进行处理。二、项目设计思路2.1 硬件设计(1)主控芯片:STM32系列MCU,负责驱动其他外设模块;(2)血氧浓度传感器:使用MAX30102血氧浓度检测传感器,用于采集人体的心跳和血氧浓度参数;(3)OLED屏:用于显示现在的心跳和血氧浓度;2.2 软件设计(1) 通过血氧模块采集人体的心跳和血氧浓度参数;(2) 通过OLED屏显示现在的心跳和血氧浓度;(3) 对采集到的数据进行指标分析,将采集到的数据与正常指标比对,分析被检测人员的健康状态;(4) 采集的数据可通过蓝牙或WiFi传递给手机APP进行处理。2.3 技术实现(1)设计采用AD8232心电图(ECG)模块和MAX30102血氧模块采集心跳和血氧浓度参数,并通过I2C接口连接主控芯片STM32。(2)OLED屏使用I2C接口与主控芯片STM32连接。(3)采集到的数据通过算法进行指标分析,将采集到的数据与正常指标比对,判断被检测人员的健康状态。(4)设备通过蓝牙或WiFi将采集到的数据传递给手机APP进行处理。三、代码设计3.1 MAX30102血氧模块代码I2C协议代码: #define MAX30102_I2C_ADDR 0xAE ​ void MAX30102_I2C_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; I2C_InitTypeDef I2C_InitStructure; ​ /* Enable GPIOB clock */ RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); /* Enable I2C1 and I2C2 clock */ RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1 | RCC_APB1Periph_I2C2, ENABLE); ​ // Configure I2C SCL and SDA pins GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8 | GPIO_Pin_9; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD; // Open-drain output GPIO_Init(GPIOB, &GPIO_InitStructure); ​ // Configure I2C parameters I2C_InitStructure.I2C_Mode = I2C_Mode_I2C; I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2; I2C_InitStructure.I2C_OwnAddress1 = 0x00; I2C_InitStructure.I2C_Ack = I2C_Ack_Enable; I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit; I2C_InitStructure.I2C_ClockSpeed = 100000; // 100KHz I2C_Init(I2C1, &I2C_InitStructure); ​ // Enable I2C I2C_Cmd(I2C1, ENABLE); } ​ void MAX30102_I2C_WriteReg(uint8_t reg, uint8_t value) { while(I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY)); ​ I2C_GenerateSTART(I2C1, ENABLE); while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT)); ​ I2C_Send7bitAddress(I2C1, MAX30102_I2C_ADDR, I2C_Direction_Transmitter); while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)); ​ I2C_SendData(I2C1, reg); while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED)); ​ I2C_SendData(I2C1, value); while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED)); ​ I2C_GenerateSTOP(I2C1, ENABLE); } ​ uint8_t MAX30102_I2C_ReadReg(uint8_t reg) { uint8_t value; ​ while(I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY)); ​ I2C_GenerateSTART(I2C1, ENABLE); while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT)); ​ I2C_Send7bitAddress(I2C1, MAX30102_I2C_ADDR, I2C_Direction_Transmitter); while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)); ​ I2C_SendData(I2C1, reg); while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED)); ​ I2C_GenerateSTART(I2C1, ENABLE); while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT)); ​ I2C_Send7bitAddress(I2C1, MAX30102_I2C_ADDR, I2C_Direction_Receiver); while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED)); ​ I2C_AcknowledgeConfig(I2C1, DISABLE); value = I2C_ReceiveData(I2C1); ​ I2C_GenerateSTOP(I2C1, ENABLE); ​ return value; } ​ void MAX30102_I2C_ReadArray(uint8_t reg, uint8_t* data, uint8_t len) { while(I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY)); ​ I2C_GenerateSTART(I2C1, ENABLE); while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT)); ​ I2C_Send7bitAddress(I2C1, MAX30102_I2C_ADDR, I2C_Direction_Transmitter); while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)); ​ I2C_SendData(I2C1, reg); while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED)); ​ I2C_GenerateSTART(I2C1, ENABLE); while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT)); ​ I2C_Send7bitAddress(I2C1, MAX30102_I2C_ADDR, I2C_Direction_Receiver); while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED)); ​ while(len > 1) { I2C_AcknowledgeConfig(I2C1, ENABLE); while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED)); *data++ = I2C_ReceiveData(I2C1); len--; } ​ I2C_AcknowledgeConfig(I2C1, DISABLE); while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED)); *data++ = I2C_ReceiveData(I2C1); ​ I2C_GenerateSTOP(I2C1, ENABLE); }MAX30102的初始化函数和数据获取函数: void MAX30102_Init(void) { MAX30102_I2C_Init(); ​ // Reset the device MAX30102_I2C_WriteReg(0x09, 0x40); HAL_Delay(100); MAX30102_I2C_WriteReg(0x09, 0x00); ​ // Set FIFO average to 4 samples MAX30102_I2C_WriteReg(0x08, 0x03); ​ // Set LED pulse amplitude MAX30102_I2C_WriteReg(0x0C, 0x1F); MAX30102_I2C_WriteReg(0x0D, 0x1F); ​ // Set sample rate to 100Hz MAX30102_I2C_WriteReg(0x0F, 0x04); ​ // Enable the red LED only MAX30102_I2C_WriteReg(0x11, 0x02); ​ // Read the temperature value to start a reading MAX30102_I2C_ReadReg(0x1F); } ​ uint32_t MAX30102_GetHeartRate(void) { uint8_t buffer[MAX30102_FIFO_DEPTH*4]; MAX30102_Data sensor_data = {0}; uint16_t ir_value; uint16_t red_value; uint8_t byte_count, fifo_overflow; ​ // Check if any data is available in FIFO byte_count = MAX30102_I2C_ReadReg(0x06) - MAX30102_I2C_ReadReg(0x04); if(byte_count > 0) { fifo_overflow = MAX30102_I2C_ReadReg(0x09) & 0x80; ​ // Read the data from FIFO MAX30102_I2C_ReadArray(0x07, buffer, byte_count); ​ // Parse the data for(int i=0; i<byte_count; i+=4) { ir_value = ((uint16_t)buffer[i] << 8) | buffer[i+1]; red_value = ((uint16_t)buffer[i+2] << 8) | buffer[i+3]; ​ // Update the sensor data MAX30102_UpdateData(&sensor_data, ir_value, red_value); } ​ if(!fifo_overflow && MAX30102_CheckForBeat(sensor_data.IR_AC_Signal_Current)) { return MAX30102_HeartRate(sensor_data.IR_AC_Signal_Previous, 16); } } ​ return 0; }数据处理函数: void MAX30102_UpdateData(MAX30102_Data* data, uint16_t ir_value, uint16_t red_value) { int32_t ir_val_diff = ir_value - data->IR_AC_Signal_Current; int32_t red_val_diff = red_value - data->Red_AC_Signal_Current; ​ // Update IR AC and DC signals data->IR_AC_Signal_Current = (ir_val_diff + (7 * data->IR_AC_Signal_Previous)) / 8; data->IR_DC_Signal_Current = (ir_value + data->IR_AC_Signal_Current + (2 * data->IR_DC_Signal_Current)) / 4; data->IR_AC_Signal_Previous = data->IR_AC_Signal_Current; ​ // Update Red AC and DC signals data->Red_AC_Signal_Current = (red_val_diff + (7 * data->Red_AC_Signal_Previous)) / 8; data->Red_DC_Signal_Current = (red_value + data->Red_AC_Signal_Current + (2 * data->Red_DC_Signal_Current)) / 4; data->Red_AC_Signal_Previous = data->Red_AC_Signal_Current; ​ // Update IR and Red AC signal peak-to-peak values if(data->IR_AC_Signal_Current > data->IR_AC_Max) data->IR_AC_Max = data->IR_AC_Signal_Current; else if(data->IR_AC_Signal_Current < data->IR_AC_Min) data->IR_AC_Min = data->IR_AC_Signal_Current; ​ if(data->Red_AC_Signal_Current > data->Red_AC_Max) data->Red_AC_Max = data->Red_AC_Signal_Current; else if(data->Red_AC_Signal_Current < data->Red_AC_Min) data->Red_AC_Min = data->Red_AC_Signal_Current; } ​ uint8_t MAX30102_CheckForBeat(int32_t ir_val) { static uint8_t beat_detection_enabled = 1; static uint32_t last_beat_time = 0; static int32_t threshold = 0x7FFFFF; ​ uint32_t delta_time; int32_t beat_amplitude; ​ if(beat_detection_enabled) { // Increment the beat counter MAX30102_beat_counter++; ​ // Calculate the threshold value threshold += (ir_val - threshold) / 8; ​ // Check if a beat has occurred if(ir_val > threshold && MAX30102_beat_counter > 20) { delta_time = micros() - last_beat_time; last_beat_time = micros(); beat_amplitude = ir_val - threshold; if(delta_time < 1000 || delta_time > 2000 || beat_amplitude < 20 || beat_amplitude > 1000) { return 0; } // Reset the beat counter and set the threshold value MAX30102_beat_counter = 0; threshold = ir_val; ​ return 1; } } ​ return 0; } ​ uint32_t MAX30102_HeartRate(int32_t ir_val, uint8_t samples) { int32_t ir_val_sum = 0; // Calculate the sum of IR values for(int i=0; i<samples; i++) { ir_val_sum += MAX30102_IR_Sample_Buffer[i]; } ​ // Calculate the average IR value ir_val_sum /= samples; ​ // Calculate the heart rate return (uint32_t)(60 * MAX30102_SAMPLING_FREQUENCY / (ir_val - ir_val_sum)); }3.2 OLED显示屏驱动代码I2C协议代码: #define SSD1306_I2C_ADDR 0x78 ​ void SSD1306_I2C_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; I2C_InitTypeDef I2C_InitStructure; ​ /* Enable GPIOB clock */ RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); /* Enable I2C1 and I2C2 clock */ RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1 | RCC_APB1Periph_I2C2, ENABLE); ​ // Configure I2C SCL and SDA pins GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8 | GPIO_Pin_9; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD; // Open-drain output GPIO_Init(GPIOB, &GPIO_InitStructure); ​ // Configure I2C parameters I2C_InitStructure.I2C_Mode = I2C_Mode_I2C; I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2; I2C_InitStructure.I2C_OwnAddress1 = 0x00; I2C_InitStructure.I2C_Ack = I2C_Ack_Enable; I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit; I2C_InitStructure.I2C_ClockSpeed = 100000; // 100KHz I2C_Init(I2C1, &I2C_InitStructure); ​ // Enable I2C I2C_Cmd(I2C1, ENABLE); } ​ void SSD1306_I2C_WriteReg(uint8_t reg, uint8_t value) { while(I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY)); ​ I2C_GenerateSTART(I2C1, ENABLE); while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT)); ​ I2C_Send7bitAddress(I2C1, SSD1306_I2C_ADDR, I2C_Direction_Transmitter); while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)); ​ I2C_SendData(I2C1, 0x00); while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED)); ​ I2C_SendData(I2C1, reg); while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED)); ​ I2C_SendData(I2C1, value); while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED)); ​ I2C_GenerateSTOP(I2C1, ENABLE); } ​ void SSD1306_I2C_WriteArray(uint8_t* data, uint16_t len) { while(I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY)); ​ I2C_GenerateSTART(I2C1, ENABLE); while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT)); ​ I2C_Send7bitAddress(I2C1, SSD1306_I2C_ADDR, I2C_Direction_Transmitter); while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)); ​ while(len--) { I2C_SendData(I2C1, *data++); while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED)); } ​ I2C_GenerateSTOP(I2C1, ENABLE); }SSD1306的初始化函数和数据更新函数: #define SSD1306_WIDTH 128 #define SSD1306_HEIGHT 64 #define SSD1306_BUFFER_SIZE (SSD1306_WIDTH*SSD1306_HEIGHT/8) ​ uint8_t SSD1306_Buffer[SSD1306_BUFFER_SIZE]; ​ void SSD1306_Init(void) { SSD1306_I2C_Init(); ​ // Turn display off SSD1306_DisplayOff(); ​ // Set the clock to a high value for faster data transfer SSD1306_I2C_WriteReg(0x0F, 0x80); ​ // Set multiplex ratio to default value (63) SSD1306_I2C_WriteReg(0xA8, 0x3F); ​ // Set the display offset to 0 SSD1306_I2C_WriteReg(0xD3, 0x00); ​ // Display start line is 0 SSD1306_I2C_WriteReg(0x40, 0x00); ​ // Set segment remap to inverted SSD1306_I2C_WriteReg(0xA1, 0xC0); ​ // Set COM output scan direction to inverted SSD1306_I2C_WriteReg(0xC8, 0xC0); ​ // Disable display offset shift SSD1306_I2C_WriteReg(0xD7, 0x9F); ​ // Set display clock divide ratio/oscillator frequency to default value (8/0xF0) SSD1306_I2C_WriteReg(0xD5, 0xF0); ​ // Enable charge pump regulator SSD1306_I2C_WriteReg(0x8D, 0x14); ​ // Set memory addressing mode // Set the display to normal mode (not inverted) SSD1306_I2C_WriteReg(0xA6, 0xA6); ​ // Set the contrast to a default value of 127 SSD1306_I2C_WriteReg(0x81, 0x7F); ​ // Turn the display back on SSD1306_DisplayOn(); ​ // Clear the display buffer SSD1306_ClearBuffer(); ​ // Update the display with the cleared buffer SSD1306_UpdateDisplay(); } ​ void SSD1306_UpdateDisplay(void) { uint8_t column, page; }for(page=0; page<8; page++) { SSD1306_I2C_WriteReg(0xB0+page, 0x00); SSD1306_I2C_WriteReg(0x10, 0x00); SSD1306_I2C_WriteReg(0x00, 0x00); ​ for(column=0; column<SSD1306_WIDTH; column++) { SSD1306_I2C_WriteArray(&SSD1306_Buffer[column + page*SSD1306_WIDTH], 1); } } } void SSD1306_ClearBuffer(void) { memset(SSD1306_Buffer, 0x00, sizeof(SSD1306_Buffer)); } ​ void SSD1306_SetPixel(uint8_t x, uint8_t y, uint8_t color) { if(x >= SSD1306_WIDTH || y >= SSD1306_HEIGHT) { return; } }if(color) { SSD1306_Buffer[x + (y/8)*SSD1306_WIDTH] |= (1 << (y%8)); } else { SSD1306_Buffer[x + (y/8)*SSD1306_WIDTH] &= ~(1 << (y%8)); } }四、总结本设计采用STM32为主控芯片,配合血氧浓度传感器和OLED屏幕,实现了人体健康数据的采集和展示,并通过算法对采集到的数据进行分析,判断被检测人员的健康状态。同时,设计使用蓝牙或WiFi将采集到的数据传递给手机APP进行处理。设计基本满足了人体健康检测仪的技术要求和环境要求。
  • [技术干货] 基于STM32设计的门禁照相机
    一、项目介绍当前文章介绍基于STM32设计的门禁照相机,本项目提供了一种更加智能、安全、便捷的门禁解决方案。门禁照相机采用STM32F103ZET6 MCU作为主控芯片,配合2.8寸LCD显示屏、OV7725数字摄像头、SD卡和模拟门铃按键等外设模块,实现了摄像头画面实时显示、门铃触发拍照、图片存储等功能。在使用该门禁照相机时,来访客人只需按下门铃按键,摄像头即可自动拍摄照片并保存到SD卡中。同时,用户也可以通过LCD屏幕进行时间调整和本地图片浏览等操作,提高了门禁系统的可操作性和用户体验。门禁照相机的设计为了提高门禁系统的安全性和智能化程度,解决传统门禁系统存在的诸多问题。通过采用数字摄像头替代传统猫眼,并实现照片自动拍摄和存储功能,有效提高了门禁系统的安全性。同时,通过LCD屏幕进行时间调整和本地图片浏览等操作,实现了门禁系统的智能化,提高了用户的使用体验。二、硬件设计本照相机的主要硬件包括 STM32F103ZET6 MCU、3.5寸 LCD 显示屏、OV7725 数字摄像头、SD 卡和一个模拟门铃的按键。(1)STM32F103ZET6 MCUSTM32F103ZET6 MCU 是本照相机的主控芯片,它可以通过 GPIO 口驱动其他外设模块。(2)3.5寸 LCD 显示屏3.5寸 LCD 显示屏可以实时显示摄像头捕捉的画面,并且支持多个页面的切换。(3)OV7725 数字摄像头OV7725 数字摄像头可以采集来访客人的画面,并将其实时显示在 LCD 显示屏上。(4)SD 卡SD 卡用于存储照相机拍摄的照片,照片的名称由当前时间日期命名。(5)模拟门铃的按键模拟门铃的按键用于触发照相机拍摄照片。三、软件设计3.1 技术要求(1)实时显示画面本照相机通过 OV7725 数字摄像头捕捉来访客人的画面,并通过3.5寸 LCD 显示屏实时显示。(2)拍照并保存到 SD 卡当有来访者按下模拟门铃按键时,照相机会拍摄照片并保存到 SD 卡中,照片名称以当前时间日期命名。3.2 软件流程(1) 初始化STM32F103ZET6 MCU及外部设备; (2) 启动OV7725 数字摄像头; (3) 开启LCD; (4) 进入主循环: a. 读取按键状态是否是门铃被按下; b. 若发现门铃按下,则照相机开始拍照并将照片保存到 SD 卡; c. 更新屏幕上的内容。3.3 代码实现代码实现过程:(1) 初始化STM32F103ZET6 MCU及外部设备;(2) 启动OV7725 数字摄像头,并设置其采集参数;(3) 初始化 SD 卡,并在 SD 卡上创建一个文件夹用于存储照片;(4) 开启LCD,并设置其显示参数;(5) 进入主循环,读取按键状态并更新屏幕上的内容;(6) 当发现门铃被按下时,开始拍照并将照片保存到 SD 卡中。四、代码实现4.1 整体代码框架 #include "stm32f10x.h" #include "sdio_sdcard.h" #include "ff.h" ​ /* 定义一些宏和变量 */ ​ int main(void) { /* 初始化系统时钟(例如使用 HSE 8MHz 作为系统时钟) */ /* 初始化 GPIO 端口、SDIO、LCD、OV7725 等外设模块 */ /* 初始化 SD 卡,并在其上创建用于存储照片的文件夹 */ ​ while(1) { /* 读取门铃按键状态 */ if(/* 检测到门铃被按下 */) { /* 拍摄照片并保存到 SD 卡中,照片名以当前时间日期命名 */ } ​ /* 更新屏幕显示内容,包括实时摄像头画面、时间日期、照片预览等 */ } }4.2 拍照存储下面是采用 STM32 的HAL 库设计的代码,控制OV7725 拍照保存为 BMP 图片到 SD 卡中。 #include "stm32f1xx_hal.h" #include "sdio_sdcard.h" #include "ff.h" #include "ov7725.h" ​ extern SD_HandleTypeDef hsd; extern DCMI_HandleTypeDef hdcmi; extern DMA_HandleTypeDef hdma_dcmi; ​ FATFS fs; FIL file; UINT bw; ​ /* 定义一些宏和变量 */ ​ int main(void) { /* 初始化系统时钟(例如使用 HSE 8MHz 作为系统时钟) */ /* 初始化 GPIO 端口、SDIO、LCD、OV7725 等外设模块 */ /* 初始化 SD 卡,并在其上创建用于存储照片的文件夹 */ ​ while(1) { /* 读取门铃按键状态 */ if(/* 检测到门铃被按下 */) { /* 拍摄照片并保存到 SD 卡中,照片名以当前时间日期命名 */ HAL_GPIO_WritePin(OV7725_RESET_GPIO_Port, OV7725_RESET_Pin, GPIO_PIN_SET); // 复位 OV7725 HAL_Delay(50); HAL_GPIO_WritePin(OV7725_RESET_GPIO_Port, OV7725_RESET_Pin, GPIO_PIN_RESET); ​ ov7725_init(); // 初始化 OV7725 ​ HAL_DCMI_Start_DMA(&hdcmi, DCMI_MODE_SNAPSHOT, (uint32_t)RGB565_buffer, (uint32_t)(CAMERA_RWIDTH * CAMERA_RHEIGH), 1); // 开始采集 ​ HAL_DMA_PollForTransfer(&hdma_dcmi, HAL_DMA_FULL_TRANSFER, 1000); // 等待采集完成 ​ /* 将 RGB565 数据转换成 BMP 格式 */ uint16_t bmp_header[54/2] = {0x4D42, 54+CAMERA_RWIDTH*CAMERA_RHEIGH*3, 0, 0, 54, 40, CAMERA_RWIDTH, CAMERA_RHEIGH, 1, 24, 0, CAMERA_RWIDTH*CAMERA_RHEIGH*3, 0, 0, 0, 0}; uint8_t bmp_data[CAMERA_RWIDTH*CAMERA_RHEIGH*3]; uint16_t i = 0, j = 0; for(i = 0; i < 54/2; i++) { bmp_data[i*2] = bmp_header[i]; // 拷贝 BMP 文件头 bmp_data[i*2+1] = bmp_header[i]>>8; } for(i = 0; i < CAMERA_RWIDTH*CAMERA_RHEIGH; i++) { bmp_data[54+i*3+0] = RGB565_buffer[i]>>8; // RGB565 转换为 BMP 格式的 RGB 24位色 bmp_data[54+i*3+1] = RGB565_buffer[i]>>3; bmp_data[54+i*3+2] = RGB565_buffer[i]<<3; } ​ /* 保存 BMP 图片到 SD 卡中 */ if(f_mount(&fs, SD_Path, 1) == FR_OK) { // 挂载 SD 卡 char filename[20]; /* 将文件名设置为当前时间日期,例如"202206151243.bmp" */ sprintf(filename, "%04d%02d%02d%02d%02d%02d.bmp", year, month, day, hour, minute, second); ​ if(f_open(&file, filename, FA_CREATE_ALWAYS | FA_WRITE) == FR_OK) { // 创建文件并写入数据 f_write(&file, bmp_data, sizeof(bmp_data), &bw); f_close(&file); } } ​ HAL_Delay(1000); // 防止连续拍照 } ​ /* 更新屏幕显示内容,包括实时摄像头画面、时间日期、照片预览等 */ } }
  • [技术干货] SQLite数据库完成数据增删改查
    一、项目介绍当前文章介绍的设计的主要功能是利用 SQLite 数据库实现宠物投喂器上传数据的存储,并且支持数据的增删改查操作。其中,宠物投喂器上传的数据包括投喂间隔时间、水温、剩余重量等参数。实现功能:创建 SQLite 数据库表,用于存储宠物投喂器上传的数据。实现对数据库表中数据的插入操作,即将从宠物投喂器接收到的数据存储到数据库中。实现对数据库表中数据的查询操作,包括按照投喂间隔时间、水温、剩余重量等参数进行筛选,以便用户能够查看特定范围内的数据信息。实现对数据库表中数据的修改操作,即可以修改已经存储的宠物投喂器上传的数据。实现对数据库表中数据的删除操作,即可以删除已经存储的宠物投喂器上传的数据。二、SQLite数据库SQLite是一款轻量级、开源的嵌入式关系型数据库管理系统(RDBMS),设计目标是嵌入式设备或应用程序使用。与传统的客户端/服务器模式不同,SQLite引擎不是一个独立的进程,而是被集成在一个应用程序中。应用程序可以访问SQLite数据库文件,读写其中的数据,从而实现数据的存储和管理。以下是 SQLite 数据库的特点:轻量级:SQLite 占用资源较小,运行速度快,并且可以很方便地集成到应用程序中,使其成为一个内嵌的数据库。无需服务器:SQLite 是一款本地化的数据库,无需专门的服务器进行支持,因此对于小型应用程序来说,是一种非常适合的解决方案。开源:SQLite 是一款开源的数据库,用户可以免费获取其源代码,并且可以自由地进行修改和定制。支持 SQL:SQLite 支持完整的 SQL 标准,并且在 SQL 语法和命令方面与其他关系型数据库非常接近,具有较高的兼容性。可移植性:SQLite 支持多种操作系统和编程语言,如 Windows、Linux、Mac OS X、iOS、Android 等平台,以及 C/C++、Java、Python、C# 等编程语言。数据库存储方式: SQLite 将数据库存储在单个文件中,用户可以根据需要将其复制或移动到其他位置或计算机中,以方便数据的安全备份和分享。三、在Qt里使用SQLITE数据库在 Qt 中,使用 SQLite 数据库的主要流程如下:(1)导入 SQLite 相关库文件:在 Qt 项目中,需要先导入 SQLite 相关的库文件和头文件,以便在代码中使用 SQLite 的相关函数和类。需要在项目文件中添加以下语句: QT += sql这样就可以包含 SQLite 数据库支持的相关头文件和类。(2)创建数据库连接:使用 QSqlDatabase 类可以在 Qt 中创建一个数据库连接。需要设置数据库类型(如 "QSQLITE"),以及数据库文件路径等参数。代码示例如下: QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE"); db.setDatabaseName("mydatabase.db");在实际使用时,可以使用绝对或相对路径指定数据库文件路径。(3)打开数据库:使用 QSqlDatabase 类的 open 函数可以打开数据库连接。在成功打开数据库后,可以执行 SQL 查询语句,读取和修改数据库中的数据。代码示例: if(db.open()) { QSqlQuery query; query.exec("CREATE TABLE mytable (id INTEGER PRIMARY KEY, name TEXT)"); query.exec("INSERT INTO mytable VALUES(1, 'John')"); query.exec("SELECT id, name FROM mytable"); while(query.next()) { int id = query.value(0).toInt(); QString name = query.value(1).toString(); qDebug() << id << name; } }以上代码创建了一个名为 "mytable" 的数据库表,并向其中插入了一条记录。随后,执行 SELECT 查询语句读取表中的数据,并将结果输出到控制台中。(4)关闭数据库:当不再需要使用数据库时,应该使用 close 函数关闭数据库连接,以释放资源。代码示例: db.close();在以上流程中,使用 QSqlQuery 类可以执行 SQL 查询语句,并获取查询结果。通过 QSqlRecord 类可以访问查询结果中的字段和值。四、完整代码下面是 Qt(C++)中利用 SQLite 数据库对宠物投喂器上传的数据进行存储管理的实现代码,包括数据的增删改查功能: #include <QtSql> #include <QDebug> ​ // 创建或打开数据库连接 bool createConnection() { QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE"); db.setDatabaseName("petfeeder.db"); if (!db.open()) { qDebug() << "Failed to connect database."; return false; } // 创建 petfeeder 表 QSqlQuery query; bool ret = query.exec("CREATE TABLE IF NOT EXISTS petfeeder " "(id INTEGER PRIMARY KEY AUTOINCREMENT, " "interval INTEGER, temperature REAL, weight REAL)"); if (!ret) { qDebug() << "Failed to create table: " << query.lastError().text(); } return true; } ​ // 插入数据 void insertData(int interval, double temperature, double weight) { QSqlQuery query; QString sql = QString("INSERT INTO petfeeder (interval, temperature, weight) " "VALUES (%1, %2, %3)").arg(interval).arg(temperature).arg(weight); bool ret = query.exec(sql); if (!ret) { qDebug() << "Failed to insert data: " << query.lastError().text(); } } ​ // 更新数据 void updateData(int id, int interval, double temperature, double weight) { QSqlQuery query; QString sql = QString("UPDATE petfeeder SET interval=%1, temperature=%2, weight=%3 " "WHERE id=%4").arg(interval).arg(temperature).arg(weight).arg(id); bool ret = query.exec(sql); if (!ret) { qDebug() << "Failed to update data: " << query.lastError().text(); } } ​ // 删除数据 void deleteData(int id) { QSqlQuery query; QString sql = QString("DELETE FROM petfeeder WHERE id=%1").arg(id); bool ret = query.exec(sql); if (!ret) { qDebug() << "Failed to delete data: " << query.lastError().text(); } } ​ // 查询数据 void queryData() { QSqlQuery query("SELECT * FROM petfeeder"); while (query.next()) { int id = query.value(0).toInt(); int interval = query.value(1).toInt(); double temperature = query.value(2).toDouble(); double weight = query.value(3).toDouble(); qDebug() << "Id:" << id << "Interval:" << interval << "Temperature:" << temperature << "Weight:" << weight; } } ​ // 主函数 int main() { if (!createConnection()) { return 1; } // 插入数据 insertData(3, 25.5, 0.2); insertData(2, 26, 0.3); insertData(4, 24, 0.4); // 查询数据 queryData(); // 更新数据 updateData(2, 4, 27, 0.3); // 删除数据 deleteData(3); // 查询数据 queryData(); ​ return 0; }在上面代码里,使用 createConnection 函数创建或打开数据库连接,创建名为 petfeeder 的数据表。使用 insertData 函数向数据表中插入数据,使用 updateData 函数更新数据,使用 deleteData 函数删除数据,使用 queryData 函数查询数据,将结果输出到控制台。
  • [技术干货] C语言里变量的生命周期
    在 C 语言中,变量的生命周期指的是该变量存在的时间段,理解变量的内存释放时机,设计程序才能少出问题。在程序执行期间,变量会经历以下三个阶段:(1)定义阶段(定义变量):在定义变量时,编译器会为该变量分配内存空间。此时变量的值是不确定的。(2)使用阶段(赋值、读取变量):在程序执行过程中,可以对变量进行赋值或读取操作。此时变量的值是确定的,并且会随着程序执行的进度而变化。(3)销毁阶段(变量被销毁):在变量的作用域结束时,该变量就会被销毁。在这个过程中,编译器会自动释放该变量所占用的内存空间。根据变量的定义位置和作用域,C 语言中的变量可以分为以下两种类型:(1)局部变量:定义在函数内部或代码块内部的变量称为局部变量。局部变量只能在其定义所在的函数或代码块内部使用,并且在函数或代码块结束时被销毁。局部变量的生命周期受限于其所处的函数或代码块的生命周期。(2)全局变量:定义在函数外部或文件顶部的变量称为全局变量。全局变量可以在整个程序中使用,其生命周期从程序开始到程序结束。全局变量在程序运行期间一直存在,并且在程序结束时才被销毁。除了上述两种变量类型之外,C 语言还提供了另外一种特殊的变量类型——静态变量。静态变量定义在函数内部或代码块内部,但其生命周期与局部变量不同。静态变量在函数或代码块结束时不会被销毁,而是继续存在于内存中,并保留其上一次赋值的值,直到下一次被修改。在 C 语言中,变量的生命周期是由其作用域和定义位置决定的。正确地管理变量的生命周期对于程序的正确性和性能都至关重要,程序员需要深入了解变量的生命周期,遵循正确的使用规则,确保程序的正确性和健壮性。以下是使用代码进行举例说明变量的生命周期:(1)定义阶段在定义变量时,编译器会为该变量分配内存空间。例如,在函数内部定义一个整型变量 a,其定义语句如下: void foo() { int a; // 定义变量 }此时变量 a 就被分配了内存空间,但其值是不确定的。(2)使用阶段在程序执行过程中,可以对变量进行赋值或读取操作。例如,在上述定义变量的基础上,给变量 a 赋值并读取其值的代码如下: void foo() { int a; // 定义变量 ​ a = 10; // 给变量赋值 printf("a = %d\n", a); // 打印变量的值 }此时变量 a 的值已经确定为 10,并被输出到控制台。(3)销毁阶段在变量的作用域结束时,该变量就会被销毁。在这个过程中,编译器会自动释放该变量所占用的内存空间。例如,在上述定义变量和使用变量的代码基础上,添加一个条件语句使得变量 a 在条件成立之后被销毁,示例代码如下: void foo() { int a; // 定义变量 ​ a = 10; // 给变量赋值 printf("a = %d\n", a); // 打印变量的值 ​ if (a > 5) { int b = 20; // 定义变量 printf("b = %d\n", b); // 打印变量的值 } ​ printf("a = %d\n", a); // 打印变量的值,此时变量依然存在 }在上述代码中,当条件 a > 5 成立时,程序会在条件中定义并使用一个新的整型变量 b,但该变量在条件结束后就被释放了。而变量 a 的生命周期则受限于函数 foo() 的作用域,即在函数结束时被销毁。(4)子函数返回地址(指针)如果子函数返回指针变量,需要注意指针变量的生命周期问题,以避免指针失效和内存泄漏等问题。假设有一个子函数 get_string(),该函数返回一个动态分配的字符串指针。函数定义及示例代码如下: char* get_string() { char* str = (char*) malloc(10 * sizeof(char)); str[0] = 'H'; str[1] = 'e'; str[2] = 'l'; str[3] = 'l'; str[4] = 'o'; str[5] = '\0'; return str; } ​ int main() { char* s = get_string(); printf("%s\n", s); // 输出 "Hello" ​ // 此处应该手动释放内存 free(s); ​ return 0; }在上述代码中,函数 get_string() 动态分配了一个长度为 10 的字符数组 str,并返回了该数组的首地址,该指针是在堆(heap)上分配的。由于是动态分配的内存空间,因此需要手动释放。在 main() 函数中对指针进行操作后,也需要手动释放该指针所指向的内存空间,以避免内存泄漏。以下是一个错误的示例,用于和前面正确示例进行对比,帮助理解返回指针的生命周期问题: char* get_string() { char str[] = "Hello"; return str; } ​ int main() { char* s = get_string(); printf("%s\n", s); // 输出 "Hello" ​ return 0; }在这个示例中,函数 get_string() 返回了一个局部数组 str 的首地址。由于 str 是在函数内部定义的局部变量,其生命周期仅限于函数调用过程中。当函数 get_string() 执行完毕后,str 的生命周期已经结束,其内存空间已被回收,此时返回的指针变量 s 已经成为了野指针,指向了无效的内存空间,进而会导致未定义的行为。尽管该函数定义的返回类型是 char*,但是由于返回了一个局部变量的指针,会导致指针失效、访问非法内存等问题,从而产生程序崩溃等错误行为。总结:如果一个子函数需要返回指针变量,需要确保返回的指针指向的内存空间在使用期间有效,否则会导致严重的问题。
  • [技术干货] Qt使用kingbase数据库存储数据(完成考勤系统数据增删改查)
    一、功能介绍当前基于Qt(C++)开发了一款教室上课考勤系统的软件,主要是使用了Kingbase数据库进行数据存储和管理。完成的具体功能如下:(1)功能齐全:软件可以完成学生、教师和管理员的登陆和注册,教师可以发布课程信息和考勤信息,学生可以查看自己的课程信息和考勤记录,管理员可以对教师和学生信息进行管理。软件具有数据可视化等功能,方便管理员直观地了解教学情况。(2)高效稳定:采用了Kingbase数据库存储数据,保证了数据存储的可靠性和一致性,同时也提高了系统性能和响应速度。在程序设计方面采用了MVC模式,将程序的逻辑与界面分离,使得程序结构清晰,易于维护和扩展。(3)用户友好:采用了人性化的操作界面和交互方式,让用户能够方便地浏览和管理课程和考勤记录。考虑到了软件的安全性问题,采用了哈希加密算法保护用户密码。二、Kingbase数据库介绍Kingbase是中国国产的关系型数据库管理系统,支持SQL/92标准,同时也支持PL/SQL、T-SQL等多种编程语言,拥有高性能、高可靠性、高安全性等特点,广泛应用于电信、金融、保险、能源等行业领域。Kingbase数据库是基于PostgreSQL核心技术开发的一个商业化数据库系统,因此它与PostgreSQL在许多方面相似,如语法、存储引擎和模式等。但与PostgreSQL不同的是,Kingbase数据库具有更强的自主知识产权和更丰富的中文支持,包括对汉字排序、全文检索、文字匹配等功能。Kingbase数据库支持多种操作系统平台,包括Windows、Linux、AIX、HP-UX和Solaris等。其核心技术包括:(1)分布式事务Kingbase数据库采用高效的分布式事务管理技术,可以实现多节点之间的数据一致性,并保证高可靠性和交易性能。同时也支持ACID事务属性。(2)并行查询处理Kingbase数据库采用多核、多线程的并行查询处理机制,提升查询性能和处理效率。此外,Kingbase还支持在线索引重组、预查询缓存等优化技术,可以进一步提升查询性能。(3)大数据存储和处理Kingbase数据库采用分布式数据存储和处理技术,可以支持 TB 级别的数据存储和处理。同时还支持分片、备份恢复、灾备等数据管理技术,保证数据可靠性和安全性。(4)高可用性和负载均衡Kingbase数据库支持负载均衡和高可用性集群,用户可以根据需要选择不同的部署方式,来满足业务需求。此外,Kingbase还支持在线伸缩、容错恢复等功能,加强了系统的鲁棒性和可扩展性。三、Kingbase数据库使用流程(1)安装部署Kingbase数据库软件; [1]下载安装包:从官方网站下载Kingbase数据库的安装包,根据需要选择32位或64位版本,并根据实际情况选择合适的版本号和操作系统类型。 ​ [2]运行安装程序:双击安装包,按照提示一步步进行安装。在安装过程中可以选择安装路径、开启服务等选项,需要根据实际需求进行选择。 ​ [3]配置环境变量:安装完成后,需要将Kingbase安装目录添加到系统环境变量中,以便于在命令行中直接使用Kingbase命令。具体方法是在“控制面板” -> “系统和安全” -> “系统” -> “高级系统设置” -> “环境变量”中添加环境变量KINGBASE_HOME并设置为Kingbase的安装路径。 ​ [4]启动服务:打开“服务”管理器,找到Kingbase相关的服务并启动。 ​ [5]测试连接:在命令行中输入Kingbase命令,测试是否成功连接到了数据库。也可以使用数据库客户端工具测试连接。 ​ [6]部署应用程序:如果需要在本机上部署应用程序,需要将Kingbase客户端库文件拷贝到应用程序运行目录,并在代码中指定数据库连接字符串。(2)创建Kingbase数据库用户,并授权访问数据库; 在Kingbase数据库中,创建用户并授权访问数据库的步骤: ​ [1]创建用户:使用CREATE USER语句创建用户,并设置密码。例如,创建名为“user1”的用户,密码为“123456”的SQL语句如下: CREATE USER user1 IDENTIFIED BY 123456; ​ [2]授予权限:在Kingbase中,权限控制是通过角色(或者说是用户组)来实现的。因此,需要先创建角色,并为角色分配权限,然后将用户添加到该角色中。 ​ [3]创建角色和分配权限:使用CREATE ROLE和GRANT语句创建角色并分配权限。例如,创建名为“role1”的角色,并赋予查询、插入、修改、删除表的权限的SQL语句如下: CREATE ROLE role1; GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE tablename TO role1; 这样,“role1”角色就拥有了对“tablename”表的查询、插入、修改、删除权限。 ​ [4]将用户添加到角色中:使用GRANT语句将用户添加到指定的角色中。例如,将名为“user1”的用户添加到“role1”角色中的SQL语句如下: GRANT role1 TO user1; 这样,“user1”用户就拥有了“role1”角色所拥有的权限,即对“tablename”表的查询、插入、修改、删除权限。 ​ [5]测试权限:使用“user1”用户登录数据库,测试是否可以正常访问数据库中的表。例如,在命令行中使用psql工具登录数据库,然后查询表的内容: psql -U user1 -d dbname -h localhost SELECT * FROM tablename; 如果可以正常查询到表的内容,则说明用户已经成功授权访问数据库。(3)在Qt中添加Kingbase驱动程序,并连接数据库。步骤如下:(1)Kingbase数据库软件安装和用户授权从Kingbase官方网站上下载最新版本的Kingbase数据库软件安装部署到电脑。创建Kingbase数据库用户,并授权该用户对指定的数据库进行访问。比如: 可以创建一个名为 myuser 的用户,密码为 mypassword,并授权该用户对 mydatabase 数据库进行访问。(2)添加Kingbase驱动程序在Qt中添加Kingbase驱动程序,以便于连接Kingbase数据库。在Qt的项目文件中(例如 .pro 文件),添加以下代码行:使Qt中的SQL模块可以使用。 QT += sql在 Qt Creator 编辑器中,选择菜单栏的 Tools > Options > Build & Run > Kits ,找到正在使用的编译套件,然后选择其 Compilers> C++ 基础部分。在其中,找到并选中 C++11 选项,保存更改。在pro文件中添加以下行: CONFIG += c++11在 .pro 文件中添加以下行: LIBS += -L/path/to/kingbase/libraries -lkbclient INCLUDEPATH += /path/to/kingbase/headers/path/to/kingbase/libraries 是 Kingbase 安装目录中的库文件路径,/path/to/kingbase/headers 是 Kingbase 安装目录的头文件路径。在最终的代码中添加以下行: #include <QSqlDatabase> #include <QSqlQuery> #include <QSqlError> ​ QSqlDatabase db = QSqlDatabase::addDatabase("QKBClient"); db.setHostName("localhost"); db.setDatabaseName("mydatabase"); db.setUserName("myuser"); db.setPassword("mypassword"); bool ok = db.open(); if (!ok) { qDebug() << "Failed to connect to database: " << db.lastError().text(); }创建一个名为 db 的数据库连接对象,并使用Kingbase驱动程序连接到指定的数据库。(3)使用Kingbase数据库连接成功之后,就可以使用Qt的SQL模块进行数据操作。示例代码: 使用以下代码查询数据库中的数据: QSqlQuery query; query.prepare("SELECT * FROM mytable WHERE id = :id"); query.bindValue(":id", 42); bool ok = query.exec(); if (ok) { while (query.next()) { QString name = query.value("name").toString(); int age = query.value("age").toInt(); qDebug() << "Name: " << name << ", Age: " << age; } } else { qDebug() << "Error querying database: " << query.lastError().text(); }以上代码即可查询名为 mytable 的表中 id 为 42 的记录,输出该记录的 name 和 age 字段。四、对Kingbase数据库进行增删改查下面是对Kingbase数据库的增删改查的所有功能实现代码: // 创建数据库连接 QSqlDatabase db = QSqlDatabase::addDatabase("QKBASE"); db.setHostName("localhost"); db.setPort(5432); db.setDatabaseName("mydatabase"); db.setUserName("root"); db.setPassword("password"); ​ // 打开数据库连接 if(db.open()){ qDebug() << "database open success!"; } else{ qDebug() << "database open failed!" << db.lastError().text(); } ​ // 插入数据 QSqlQuery query; query.prepare("INSERT INTO student (id, name, age) VALUES (:id, :name, :age)"); query.bindValue(":id", 1); query.bindValue(":name", "Tom"); query.bindValue(":age", 18); query.exec(); ​ // 更新数据 query.prepare("UPDATE student SET age=:age WHERE id=:id"); query.bindValue(":age", 20); query.bindValue(":id", 1); query.exec(); ​ // 删除数据 query.prepare("DELETE FROM student WHERE id=:id"); query.bindValue(":id", 1); query.exec(); ​ // 查询数据 query.exec("SELECT * FROM student"); while(query.next()){ int id = query.value(0).toInt(); QString name = query.value(1).toString(); int age = query.value(2).toInt(); qDebug() << "id:" << id << "name:" << name << "age:" << age; } ​ // 关闭数据库连接 db.close();
  • [技术干货] 基于STM32+SHT30设计的环境温度与湿度检测系统(IIC模拟时序)
    一、项目功能介绍当前介绍基于STM32F103ZCT6芯片设计的环境温度与湿度检测系统设计过程。当前系统通过SHT30温湿度传感器采集环境温度和湿度数据,并通过模拟IIC时序协议将数据传输到STM32芯片上。然后,STM32芯片通过处理这些数据并将它们显示在0.91寸OLED显示屏上,以便用户能够方便地观察环境温度和湿度的变化情况。系统的主控芯片采用了STM32F103ZCT6,这是一款高性能的32位ARM Cortex-M3微控制器,具有丰富的外设和存储器资源,可满足各种应用的需求。温湿度检测传感器采用了SHT30,这是一款高精度的数字式温湿度传感器,具有快速响应、低功耗、高可靠性等特点。为了实现数据的显示,系统采用了0.91寸OLED显示屏,驱动芯片是SSD1306,接口是IIC协议。OLED显示屏也是通过模拟IIC时序进行驱动,这种方式具有简单、可靠、低功耗等优点。(1)开发板连接SHT30实物图(2)OLED显示屏(3)测量的温度湿度串口打印二、设计思路2.1 系统硬件设计主控芯片采用STM32F103ZCT6,该芯片具有72MHz主频,具有丰富的外设资源,包括多个定时器、多个串口、多个I2C接口等。温湿度传感器采用IIC接口的SHT30,该传感器具有高精度、低功耗、数字输出等特点,可提供温度和湿度数据。OLED显示屏采用0.91寸OLED显示屏,驱动芯片是SSD1306,接口也是是IIC协议。2.2 系统软件设计系统软件设计采用STM32CubeMX和Keil MDK-ARM工具进行开发。实现步骤:(1)使用STM32CubeMX进行芯片引脚配置和初始化代码生成。(2)编写SHT30温湿度传感器的IIC通信驱动程序。(3)编写SSD1306 OLED显示屏的IIC通信驱动程序。(4)编写温湿度检测程序,通过SHT30传感器读取温度和湿度数据,并将数据显示在OLED显示屏上。(5)编写主程序,将以上各个程序整合在一起,并进行系统初始化和数据处理。2.3 系统实现(1)系统硬件实现系统硬件实现包括主控板、SHT30传感器模块和OLED显示屏模块。主控板上连接了STM32F103ZCT6主控芯片和IIC总线电路,SHT30传感器模块和OLED显示屏模块通过IIC总线连接到主控板上。(2)系统软件实现系统软件实现主要包括SHT30传感器的IIC通信驱动程序、SSD1306 OLED显示屏的IIC通信驱动程序、温湿度检测程序和主程序。其中,SHT30传感器的IIC通信驱动程序和SSD1306 OLED显示屏的IIC通信驱动程序都是基于STM32的硬件IIC接口实现的,温湿度检测程序通过SHT30传感器读取温度和湿度数据,并将数据显示在OLED显示屏上。主程序将以上各个程序整合在一起,并进行系统初始化和数据处理。三、代码实现3.1 主程序代码以下是基于STM32设计的环境温度与湿度检测系统的主函数main.c的代码实现: #include "stm32f10x.h" #include "systick.h" #include "sht30.h" #include "i2c.h" #include "oled.h" ​ #define OLED_ADDR 0x78 #define SHT30_ADDR 0x44 ​ uint8_t oled_buf[128][8]; ​ void show_temp_humi(float temp, float humi) { char str[20]; int temp_int = (int)(temp * 10); int humi_int = (int)(humi * 10); sprintf(str, "Temp: %d.%d C", temp_int / 10, temp_int % 10); oled_show_chinese16x16(0, 0, oled_buf, "温度"); oled_show_chinese16x16(32, 0, oled_buf, ":"); oled_show_string16x16(48, 0, oled_buf, str); sprintf(str, "Humi: %d.%d %%", humi_int / 10, humi_int % 10); oled_show_chinese16x16(0, 2, oled_buf, "湿度"); oled_show_chinese16x16(32, 2, oled_buf, ":"); oled_show_string16x16(48, 2, oled_buf, str); oled_refresh(0, 7, oled_buf); } ​ int main(void) { RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE); ​ i2c_init(); systick_init(72); sht30_init(SHT30_ADDR); ​ oled_init(); ​ while(1) { float temp, humi; sht30_read_temp_humi(&temp, &humi); show_temp_humi(temp, humi); delay_ms(1000); } }代码中主要实现了以下功能:(1)初始化IIC总线、SHT30传感器和OLED显示屏。(2)定时读取SHT30传感器的温度和湿度数据。(3)将温度和湿度显示在OLED显示屏上。代码中使用了systick.h、sht30.h、i2c.h和oled.h等库文件,需要将这些文件添加到工程中。其中oled.h文件提供了显示中文、字符串和刷新缓冲区等接口,可以在OLED显示屏上显示信息。具体代码实现可以参考oled.c文件。测试时,需要将OLED显示屏和SHT30传感器按照对应的引脚连接好,并将代码烧录到STM32F103ZCT6芯片中。如果一切正常,OLED显示屏上就会不断地显示当前温度和湿度值。3.2 SHT30驱动代码以下是SHT30的驱动代码:sht30.h: #ifndef __SHT30_H #define __SHT30_H ​ #include "stm32f10x.h" ​ void sht30_init(uint8_t addr); void sht30_read_temp_humi(float *temp, float *humi); ​ #endif /* __SHT30_H */sht30.c: #include "sht30.h" #include "i2c.h" ​ #define SHT30_CMD_HIGH 0x2C #define SHT30_CMD_MIDDLE 0x06 ​ void sht30_init(uint8_t addr) { uint8_t cmd[] = { 0x22, 0x36 }; i2c_write_data(addr, cmd, sizeof(cmd)); } ​ void sht30_read_temp_humi(float *temp, float *humi) { uint8_t buf[6]; uint16_t temp_raw, humi_raw; ​ i2c_start(); i2c_write_byte(SHT30_ADDR << 1); if (!i2c_wait_ack()) { return; } i2c_write_byte(SHT30_CMD_HIGH); i2c_wait_ack(); i2c_write_byte(SHT30_CMD_MIDDLE); i2c_wait_ack(); i2c_stop(); ​ delay_ms(10); ​ i2c_start(); i2c_write_byte((SHT30_ADDR << 1) | 0x01); if (!i2c_wait_ack()) { return; } for (int i = 0; i < 6; ++i) { buf[i] = i2c_read_byte(i == 5 ? 0 : 1); } i2c_stop(); ​ humi_raw = (buf[0] << 8) | buf[1]; temp_raw = (buf[3] << 8) | buf[4]; ​ *humi = 100.0f * ((float)humi_raw / 65535.0f); *temp = -45.0f + 175.0f * ((float)temp_raw / 65535.0f); }代码中定义了SHT30_CMD_HIGH和SHT30_CMD_MIDDLE两个命令,用于启动温湿度转换。在sht30_init函数中,发送初始化命令;在sht30_read_temp_humi函数中,先发送读取命令,等待10毫秒后读取温度和湿度的原始值。最后根据公式计算出实际的温度和湿度值,并将它们保存到temp和humi指针所指向的内存中。代码中调用了i2c_write_data、i2c_start、i2c_write_byte、i2c_wait_ack、i2c_read_byte和i2c_stop等IIC相关函数,这些函数的实现可以看i2c.c文件。在使用SHT30传感器之前,需要初始化IIC总线和SHT30传感器。3.3 OLED显示屏驱动代码以下是OLED显示屏相关代码:oled.h: #ifndef __OLED_H #define __OLED_H ​ #include "stm32f10x.h" ​ void oled_init(void); void oled_show_chinese16x16(uint8_t x, uint8_t y, uint8_t (*buf)[8], const char *str); void oled_show_string16x16(uint8_t x, uint8_t y, uint8_t (*buf)[8], const char *str); void oled_refresh(uint8_t page_start, uint8_t page_end, uint8_t (*buf)[8]); ​ #endif /* __OLED_H */oled.c: #include "oled.h" #include <string.h> ​ #define OLED_WIDTH 128 #define OLED_HEIGHT 64 ​ static void oled_write_cmd(uint8_t cmd) { uint8_t tx_buf[2]; tx_buf[0] = 0x00; tx_buf[1] = cmd; i2c_write_data(OLED_ADDR, tx_buf, sizeof(tx_buf)); } ​ static void oled_write_data(uint8_t data) { uint8_t tx_buf[2]; tx_buf[0] = 0x40; tx_buf[1] = data; i2c_write_data(OLED_ADDR, tx_buf, sizeof(tx_buf)); } ​ static void oled_set_pos(uint8_t x, uint8_t y) { oled_write_cmd(0xb0 + y); oled_write_cmd(((x & 0xf0) >> 4) | 0x10); oled_write_cmd(x & 0x0f); } ​ void oled_init(void) { oled_write_cmd(0xAE); //Display Off oled_write_cmd(0x00); //Set Lower Column Address oled_write_cmd(0x10); //Set Higher Column Address oled_write_cmd(0x40); //Set Display Start Line oled_write_cmd(0xB0); //Set Page Address oled_write_cmd(0x81); //Contrast Control oled_write_cmd(0xFF); //128 Segments oled_write_cmd(0xA1); //Set Segment Re-map oled_write_cmd(0xA6); //Normal Display oled_write_cmd(0xA8); //Multiplex Ratio oled_write_cmd(0x3F); //Duty = 1/64 oled_write_cmd(0xC8); //Com Scan Direction oled_write_cmd(0xD3); //Set Display Offset oled_write_cmd(0x00); oled_write_cmd(0xD5); //Set Display Clock Divide Ratio/Oscillator Frequency oled_write_cmd(0x80); oled_write_cmd(0xD9); //Set Pre-charge Period oled_write_cmd(0xF1); oled_write_cmd(0xDA); //Set COM Pins oled_write_cmd(0x12); oled_write_cmd(0xDB); //Set VCOMH Deselect Level oled_write_cmd(0x40); oled_write_cmd(0xAF); //Display On ​ memset(oled_buf, 0, sizeof(oled_buf)); } ​ void oled_show_chinese16x16(uint8_t x, uint8_t y, uint8_t (*buf)[8], const char *str) { uint16_t offset = (uint16_t)(str[0] - 0x80) * 32 + (uint16_t)(str[1] - 0x80) * 2; const uint8_t *font_data = &font_16x16[offset]; ​ for (int i = 0; i < 16; ++i) { for (int j = 0; j < 8; ++j) { uint8_t byte = font_data[i * 2 + j / 8]; uint8_t bit = (byte >> (7 - j % 8)) & 0x01; buf[y + i][x + j] = bit ? 0xff : 0x00; } } } ​ void oled_show_string16x16(uint8_t x, uint8_t y, uint8_t (*buf)[8], const char *str) { while (*str != '\0') { oled_show_chinese16x16(x, y, buf, str); x += 16; str += 2; } } ​ void oled_refresh(uint8_t page_start, uint8_t page_end, uint8_t (*buf)[8]) { for (int i = page_start; i <= page_end; ++i) { oled_set_pos(0, i); for (int j = 0; j < OLED_WIDTH; ++j) { oled_write_data(buf[i][j]); } } }代码中定义了OLED_WIDTH和OLED_HEIGHT两个常量,表示OLED显示屏的宽度和高度。在oled_init函数中,发送初始化命令,将OLED显示屏设置为正常显示模式。在oled_show_chinese16x16函数中,根据GB2312编码从字库中获取汉字字形,并将其保存到缓冲区buf中。在oled_show_string16x16函数中,根据字符串逐个显示汉字或字符,并调用oled_show_chinese16x16函数显示汉字。在oled_refresh函数中,设置页地址和列地址,并将缓冲区buf中的数据写入到OLED显示屏上。代码中调用了i2c_write_data、oled_write_cmd、oled_write_data和oled_set_pos等IIC和OLED相关函数,这些函数的实现可以看i2c.c文件。3.4 IIC模拟时序代码(SHT30)i2c.h: #ifndef __I2C_H #define __I2C_H ​ #include "stm32f10x.h" ​ void i2c_init(void); uint8_t i2c_write_data(uint8_t addr, uint8_t *data, uint8_t len); uint8_t i2c_read_data(uint8_t addr, uint8_t *data, uint8_t len); ​ #endif /* __I2C_H */i2c.c: #include "i2c.h" ​ #define I2C_SCL_PIN GPIO_Pin_6 #define I2C_SDA_PIN GPIO_Pin_7 #define I2C_SCL_PORT GPIOB #define I2C_SDA_PORT GPIOB ​ static void i2c_delay(void) { volatile int i = 7; while (i) { --i; } } ​ static void i2c_start(void) { GPIO_SetBits(I2C_SCL_PORT, I2C_SCL_PIN); GPIO_SetBits(I2C_SDA_PORT, I2C_SDA_PIN); i2c_delay(); GPIO_ResetBits(I2C_SDA_PORT, I2C_SDA_PIN); i2c_delay(); GPIO_ResetBits(I2C_SCL_PORT, I2C_SCL_PIN); i2c_delay(); } ​ static void i2c_stop(void) { GPIO_ResetBits(I2C_SDA_PORT, I2C_SDA_PIN); GPIO_SetBits(I2C_SCL_PORT, I2C_SCL_PIN); i2c_delay(); GPIO_SetBits(I2C_SDA_PORT, I2C_SDA_PIN); i2c_delay(); } ​ static uint8_t i2c_write_byte(uint8_t byte) { uint8_t ack_bit = 0; for (int i = 0; i < 8; ++i) { if ((byte & 0x80) == 0x80) { GPIO_SetBits(I2C_SDA_PORT, I2C_SDA_PIN); } else { GPIO_ResetBits(I2C_SDA_PORT, I2C_SDA_PIN); } byte <<= 1; i2c_delay(); GPIO_SetBits(I2C_SCL_PORT, I2C_SCL_PIN); i2c_delay(); GPIO_ResetBits(I2C_SCL_PORT, I2C_SCL_PIN); i2c_delay(); } ​ GPIO_SetBits(I2C_SDA_PORT, I2C_SDA_PIN); i2c_delay(); GPIO_SetBits(I2C_SCL_PORT, I2C_SCL_PIN); i2c_delay(); if (GPIO_ReadInputDataBit(I2C_SDA_PORT, I2C_SDA_PIN)) { ack_bit = 1; } GPIO_ResetBits(I2C_SCL_PORT, I2C_SCL_PIN); i2c_delay(); ​ return ack_bit; } ​ static uint8_t i2c_read_byte(uint8_t ack) { uint8_t ret = 0; GPIO_SetBits(I2C_SDA_PORT, I2C_SDA_PIN); for (int i = 0; i < 8; ++i) { ret <<= 1; GPIO_SetBits(I2C_SCL_PORT, I2C_SCL_PIN); i2c_delay(); if (GPIO_ReadInputDataBit(I2C_SDA_PORT, I2C_SDA_PIN)) { ret |= 0x01; } GPIO_ResetBits(I2C_SCL_PORT, I2C_SCL_PIN); i2c_delay(); } ​ if (ack) { GPIO_ResetBits(I2C_SDA_PORT, I2C_SDA_PIN); } else { GPIO_SetBits(I2C_SDA_PORT, I2C_SDA_PIN); } i2c_delay(); GPIO_SetBits(I2C_SCL_PORT, I2C_SCL_PIN); i2c_delay(); GPIO_ResetBits(I2C_SCL_PORT, I2C_SCL_PIN); i2c_delay(); ​ return ret; } ​ void i2c_init(void) { GPIO_InitTypeDef GPIO_InitStruct; ​ RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); ​ GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_OD; GPIO_InitStruct.GPIO_Pin = I2C_SCL_PIN | I2C_SDA_PIN; GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(I2C_SCL_PORT, &GPIO_InitStruct); ​ GPIO_SetBits(I2C_SCL_PORT, I2C_SCL_PIN); GPIO_SetBits(I2C_SDA_PORT, I2C_SDA_PIN); } ​ uint8_t i2c_write_data(uint8_t addr, uint8_t *data, uint8_t len) { i2c_start(); if (i2c_write_byte(addr << 1) == 1) { i2c_stop(); return 1; } for (int i = 0; i < len; ++i) { if (i2c_write_byte(data[i]) == 1) { i2c_stop(); return 1; } } i2c_stop(); ​ return 0; } ​ uint8_t i2c_read_data(uint8_t addr, uint8_t *data, uint8_t len) { i2c_start(); if (i2c_write_byte(addr << 1) == 1) { i2c_stop(); return 1; } for (int i = 0; i < len; ++i) { data[i] = i2c_read_byte((i == len - 1) ? 1 : 0); } i2c_stop(); ​ return 0; }上面的代码是SHT30的IIC模拟时序代码,利用GPIO模拟SCL和SDA信号线。在i2c_init函数中,初始化SCL和SDA引脚为开漏输出模式。在i2c_start函数中,发送起始位。在i2c_stop函数中,发送停止位。在i2c_write_byte函数中,按位写入字节并接收应答位。在i2c_read_byte函数中,按位读取字节并发送应答位。在i2c_write_data函数中,先发送起始位,然后发送设备地址和写方向,再发送数据,最后发送停止位。在i2c_read_data函数中,先发送起始位,然后发送设备地址和读方向,接着按字节读取数据,最后发送停止位。3.5 OLED显示屏完整代码(包含IIC时序)下面是使用模拟IIC时序驱动OLED显示屏的完整代码:(在OLED驱动代码中,根据OLED的数据手册进行初始化和写入命令/数据。)oled.h: #ifndef __OLED_H #define __OLED_H ​ #include "stm32f10x.h" ​ void oled_init(void); void oled_clear(void); void oled_display_on(void); void oled_display_off(void); void oled_draw_point(uint8_t x, uint8_t y, uint8_t mode); void oled_draw_line(uint8_t x1, uint8_t y1, uint8_t x2, uint8_t y2, uint8_t mode); void oled_draw_rectangle(uint8_t x1, uint8_t y1, uint8_t x2, uint8_t y2, uint8_t mode); void oled_draw_circle(int8_t x, int8_t y, uint8_t r, uint8_t mode); void oled_show_char(uint8_t x, uint8_t y, uint8_t chr, uint8_t size, uint8_t mode); void oled_show_string(uint8_t x, uint8_t y, const char *str, uint8_t size, uint8_t mode); void oled_show_digit(uint8_t x, uint8_t y, uint8_t n, uint8_t size, uint8_t mode); ​ #endif /* __OLED_H */oled.c:#include "oled.h"#include "i2c.h"#define OLED_WIDTH 128#define OLED_HEIGHT 64#define OLED_ADDRESS 0x78#define OLED_CMD_MODE 0x00#define OLED_DATA_MODE 0x40static uint8_t oled_buffer[OLED_WIDTH * OLED_HEIGHT / 8];static void oled_write_cmd(uint8_t cmd){ uint8_t data[2] = {OLED_CMD_MODE, cmd}; i2c_write_data(OLED_ADDRESS, data, 2);}static void oled_write_data(uint8_t *data, uint16_t len){ uint8_t buffer[17]; buffer[0] = OLED_DATA_MODE; for (int i = 0; i < len; ++i) { buffer[i + 1] = data[i]; } i2c_write_data(OLED_ADDRESS, buffer, len + 1);}static void oled_set_pos(uint8_t x, uint8_t y){ oled_write_cmd(0xb0 + y); oled_write_cmd(((x & 0xf0) >> 4) | 0x10); oled_write_cmd((x & 0x0f) | 0x01);}void oled_init(void){ i2c_init(); oled_write_cmd(0xAE); //display off oled_write_cmd(0x20); //Set Memory Addressing Mode oled_write_cmd(0x10); //00,Horizontal Addressing Mode;01,Vertical Addressing Mode;10,Page Addressing Mode (RESET);11,Invalid oled_write_cmd(0xb0); //Set Page Start Address for Page Addressing Mode,0-7 oled_write_cmd(0xc8); //Set COM Output Scan Direction oled_write_cmd(0x00); //---set low column address oled_write_cmd(0x10); //---set high column address oled_write_cmd(0x40); //--set start line address oled_write_cmd(0x81); //--set contrast control register oled_write_cmd(0xff); oled_write_cmd(0xa1); //--set segment re-map 0 to 127 oled_write_cmd(0xa6); //--set normal display oled_write_cmd(0xa8); //--set multiplex ratio(1 to 64) oled_write_cmd(0x3f); // oled_write_cmd(0xa4); //0xa4,Output follows RAM content;0xa5,Output ignores RAM content oled_write_cmd(0xd3); //-set display offset oled_write_cmd(0x00); //-not offset oled_write_cmd(0xd5); //--set display clock divide ratio/oscillator frequency oled_write_cmd(0xf0); //--set divide ratio oled_write_cmd(0xd9); //--set pre-charge period oled_write_cmd(0x22); // oled_write_cmd(0xda); //--set com pins hardware configuration oled_write_cmd(0x12); oled_write_cmd(0xdb); //--set vcomh oled_write_cmd(0x20); //0x20,0.77xVcc oled_write_cmd(0x8d); //--set DC-DC enable oled_write_cmd(0x14); // oled_write_cmd(0xaf); //--turn on oled panel oled_clear();}void oled_clear(void){ for (int i = 0; i < sizeof(oled_buffer); ++i) { oled_buffer[i] = 0x00; } for (int i = 0; i < OLED_HEIGHT / 8; ++i) { oled_set_pos(0, i); oled_write_data(oled_buffer + OLED_WIDTH * i, OLED_WIDTH); }}void oled_display_on(void){ oled_write_cmd(0xAF);}void oled_display_off(void){ oled_write_cmd(0xAE);}void oled_draw_point(uint8_t x, uint8_t y, uint8_t mode){ if ((x >= OLED_WIDTH) || (y >= OLED_HEIGHT)) { return; } if (mode) { oled_buffer[x + (y / 8) * OLED_WIDTH] |= (1 << (y % 8)); } else { oled_buffer[x + (y / 8) * OLED_WIDTH] &= ~(1 << (y % 8)); } oled_set_pos(x, y / 8); oled_write_data(&oled_buffer[x + (y / 8) * OLED_WIDTH], 1);}void oled_draw_line(uint8_t x1, uint8_t y1, uint8_t x2, uint8_t y2, uint8_t mode){ int dx, dy, sx, sy, err, e2; dx = abs((int)x2 - (int)x1); dy = abs((int)y2 - (int)y1); if (x1 < x2) { sx = 1; } else { sx = -1; } if (y1 < y2) { sy = 1; } else { sy = -1; } err = dx - dy; while (1) { oled_draw_point(x1, y1, mode); if ((x1 == x2) && (y1 == y2)) { break; } e2 = 2 * err; if (e2 > -dy) { err = err - dy; x1 = x1 + sx; } if (e2 < dx) { err = err + dx; y1 = y1 + sy; } }}void oled_draw_rectangle(uint8_t x1, uint8_t y1, uint8_t x2, uint8_t y2, uint8_t mode){ oled_draw_line(x1, y1, x2, y1, mode); oled_draw_line(x1, y1, x1, y2, mode); oled_draw_line(x1, y2, x2, y2, mode); oled_draw_line(x2, y1, x2, y2, mode);}void oled_draw_circle(int8_t x, int8_t y, uint8_t r, uint8_t mode){ int a, b, num; a = 0; b = r; while (2 * b * b >= r * r) { oled_draw_point(x + a, y - b, mode); oled_draw_point(x - a, y - b, mode); oled_draw_point(x - a, y + b, mode); oled_draw_point(x + a, y + b, mode); oled_draw_point(x + b, y + a, mode); oled_draw_point(x + b, y - a, mode); oled_draw_point(x - b, y - a, mode); oled_draw_point(x - b, y + a, mode); a++; num = -((int)a * a + (int)b * b - (int)r * r); if (num > 0) { b--; a--; } }}void oled_show_char(uint8_t x, uint8_t y, uint8_t chr, uint8_t size, uint8_t mode){ uint8_t font_size = (size / 8 + ((size % 8) ? 1 : 0)) * (size / 2); uint8_t font[font_size]; for (int i = 0; i < font_size; ++i) { #ifdef OLED_USE_FONT_8x16 font[i] = font_8x16[(chr - ' ') * font_size + i]; #else font[i] = font_6x8[(chr - ' ') * font_size + i]; #endif } for (int i = 0; i < size / 2; ++i) { for (int j = 0; j < size / 8; ++j) { for (int k = 0; k < 8; ++k) { if (font[j + i * (size / 8)] & (1 << k)) { oled_draw_point(x + j * 8 + k, y + i * 8, mode); } } } }}void oled_show_string(uint8_t x, uint8_t y, const char *str, uint8_t size, uint8_t mode){ while (*str) { oled_show_char(x, y, *str, size, mode); x += size / 2; str++; }}void oled_show_digit(uint8_t x, uint8_t y, uint8_t n, uint8_t size, uint8_t mode){ char str[2]; str[0] = n + '0'; str[1] = '\0'; oled_show_string(x, y, str, size, mode);}上面代码里oled_init函数用于初始化OLED,包括打开I2C接口、依次发送多条命令以设置OLED参数。oled_clear函数用于清除OLED屏幕上的所有显示内容。oled_display_on和oled_display_off函数用于控制OLED的开关。oled_draw_point函数用于绘制一个像素点。oled_draw_line函数用于绘制一条直线。oled_draw_rectangle函数用于绘制一个矩形。oled_draw_circle函数用于绘制一个圆形。oled_show_char函数用于绘制一个ASCII字符。oled_show_string函数可以显示一个字符串。oled_show_digit函数可以显示一个数字。四、总结本项目是基于STM32F103ZCT6芯片设计的环境温度与湿度检测系统。系统通过SHT30温湿度传感器采集环境温度和湿度数据,并通过模拟IIC时序协议将数据传输到STM32芯片上。然后,STM32芯片通过处理这些数据并将它们显示在0.91寸OLED显示屏上,以便用户能够方便地观察环境温度和湿度的变化情况。在本项目中,选择了STM32F103ZCT6作为主控芯片。该芯片具有高性能、低功耗、丰富的外设和存储器资源等特点,非常适合用于嵌入式系统设计。然后,选择了SHT30温湿度传感器作为环境温度和湿度的检测器。该传感器具有高精度、快速响应、低功耗等特点,可以准确地测量环境温度和湿度。为了实现数据的显示,采用了0.91寸OLED显示屏,驱动芯片是SSD1306,接口是IIC协议。OLED显示屏也是通过模拟IIC时序进行驱动,这种方式具有简单、可靠、低功耗等优点。在软件设计方面,使用了Keil MDK作为开发工具,并使用STM32CubeMX进行芯片初始化和外设配置。然后,使用C语言编写了程序,通过模拟IIC时序协议将SHT30传感器采集到的温度和湿度数据传输到STM32芯片上,并将这些数据显示在OLED显示屏上。同时还添加了温度和湿度的校准、数据的存储和读取等功能。在系统实现方面,进行了硬件设计、软件开发、系统调试和测试等工作。通过不断的优化和调试,最终实现了一个功能稳定、性能优良的环境温度与湿度检测系统。
  • [技术干货] 基于51单片机+SHT30设计的环境温度与湿度检测设备(IIC模拟时序)
    一、项目介绍当前文章介绍基于51单片机和SHT30传感器设计的环境温度与湿度检测设备。设备采用IIC模拟时序通信协议,能够实时监测环境的温度和湿度,并将数据通过LCD显示屏显示出来;可以广泛应用于室内环境监测、气象观测、农业温室监测等领域。在本项目中,使用了51单片机作为主控芯片,SHT30传感器作为温湿度传感器,LCD显示屏作为数据显示模块。通过51单片机的GPIO口模拟IIC通信协议,实现了与SHT30传感器的数据通信。二、硬件设计2.1 硬件构成本次设计所需的硬件主要包括以下部分:STC89C52单片机SHT30温湿度传感器串口通信模块LCD1602显示屏电源模块杜邦线等连接线2.2 硬件接口及信号本次设计使用51单片机通过IIC总线与SHT30传感器进行通信,同时使用串口与上位机进行数据传输,并使用液晶显示屏显示当前温湿度值。具体接口和信号定义如下:(1) 51单片机与SHT30传感器之间的IIC接口:端口功能说明P2.0SDA数据线P2.1SCL时钟线P2.2RESET复位线(2) 51单片机与串口通信模块之间的接口:端口功能说明P3.0TXD发送线P3.1RXD接收线P3.2GND地线(3) 51单片机与液晶屏之间的接口:端口功能说明P1.0-P1.7DB0-DB7数据线P0.0RS指令/数据选择线P0.1RW读/写选择线P0.2E使能线P0.3CS片选线VCC电源正极5VGND电源地地三、软件设计3.1 SHT30传感器代码下面代码读取SHT30传感器的值并通过串口打印。 #include <REG52.h> #include <stdio.h> ​ #define uchar unsigned char #define uint unsigned int ​ sbit SDA=P2^0; sbit SCL=P2^1; ​ void delay(int n) { int i; while(n--) { for(i=0; i<120; i++); } } ​ void start() { SDA = 1; _nop_(); SCL = 1; _nop_(); SDA = 0; _nop_(); SCL = 0; _nop_(); } ​ void stop() { SDA = 0; _nop_(); SCL = 1; _nop_(); SDA = 1; _nop_(); } ​ void ack() { SDA = 0; _nop_(); SCL = 1; _nop_(); SCL = 0; _nop_(); SDA = 1; _nop_(); } ​ void nack() { SDA = 1; _nop_(); SCL = 1; _nop_(); SCL = 0; _nop_(); } ​ void write_byte(uchar dat) { uchar i; for(i=0; i<8; i++) { SDA = dat & 0x80; _nop_(); SCL = 1; _nop_(); SCL = 0; _nop_(); dat <<= 1; } ack(); } ​ uchar read_byte() { uchar i, dat; for(i=0; i<8; i++) { dat <<= 1; SCL = 1; _nop_(); dat |= SDA; SCL = 0; _nop_(); } return dat; } ​ void init_sht30() { start(); write_byte(0x80); if(read_byte() != 0x5A) { stop(); return; } write_byte(0xBE); if(read_byte() != 0x08 || read_byte() != 0x00) { stop(); return; } stop(); } ​ float measure_temp(void) { uchar temp_h, temp_l, crc; float temp; ​ start(); write_byte(0x80); // 主机发送写地址 write_byte(0x2C); // 选择开始温度测量命令 write_byte(0x06); stop(); ​ delay(15); // 延时等待温度测量完成 ​ start(); write_byte(0x81); // 主机发送读地址 temp_h=read_byte(); ack(); temp_l=read_byte(); ack(); crc=read_byte(); stop(); ​ temp = ((temp_h<<8)+temp_l)*175.0/0xffff - 45.0; // 温度值转换公式 ​ return temp; } ​ float measure_humi(void) { uchar humi_h, humi_l, crc; float humi; ​ start(); write_byte(0x80); // 主机发送写地址 write_byte(0x2C); // 选择开始湿度测量命令 write_byte(0x06); stop(); ​ delay(15); // 延时等待湿度测量完成 ​ start(); write_byte(0x81); // 主机发送读地址 humi_h=read_byte(); ack(); humi_l=read_byte(); ack(); crc=read_byte(); stop(); ​ humi = ((humi_h<<8)+humi_l)*100.0/0xffff; // 湿度值转换公式 ​ return humi; } ​ void main() { float temp, humi; ​ init_sht30(); // SHT30 初始化 ​ TMOD=0x20; // 定时器0工作方式2,8位定时器,用于波特率设置 TH1=0xfd; // 波特率9600 TL1=0xfd; TR1=1; // 启动定时器0 ​ SCON=0x50; // 设置串口工作方式1,允许接收,允许接收中断 ES=1; // 允许串口中断 ​ while(1) { temp = measure_temp(); humi = measure_humi(); printf("Temperature: %.1fC, Humidity: %.1f%\n", temp, humi); delay(500); // 间隔时间500ms } } ​ void ser() interrupt 4 using 2 { if(RI) // 接收到数据 { RI=0; // 清除标志位 } if(TI) // 发送完毕 { TI=0; // 清除标志位 } }在上面的代码中,定义了两个函数 measure_temp 和 measure_humi,分别用于测量温度和湿度值,并返回结果。在主函数中,利用这两个函数得到当前的温湿度值,然后通过串口打印出来。3.2 LCD1602显示屏代码下面代码是LCD1602驱动代码,完成数字字符显示。 #include <REG52.h> ​ #define LCD1602_DB P0 sbit RS = P2^5; sbit RW = P2^6; sbit E = P2^7; ​ void delay(int n) { int i; while(n--) { for(i=0; i<120; i++); } } ​ void main() { //LCD 初始化 delay(1000); LCD1602_DB = 0x38; E = 1; delay(5); E = 0; ​ delay(500); LCD1602_DB = 0x08; E = 1; delay(5); E = 0; ​ delay(500); LCD1602_DB = 0x01; E = 1; delay(5); E = 0; ​ delay(500); LCD1602_DB = 0x06; E = 1; delay(5); E = 0; ​ delay(500); LCD1602_DB = 0x0C; E = 1; delay(5); E = 0; ​ while(1) { //向LCD中写入数字12345 RS = 0; //选择指令寄存器 ​ LCD1602_DB = 0x80; //设置地址为第一行的第一个字符位置(0x80 + 0x00) ​ E = 1; delay(5); E = 0; ​ RS = 1; //选择数据寄存器 ​ LCD1602_DB = 0x31; //写入数字1 E = 1; delay(5); E = 0; ​ LCD1602_DB = 0x32; //写入数字2 E = 1; delay(5); E = 0; ​ LCD1602_DB = 0x33; //写入数字3 E = 1; delay(5); E = 0; ​ LCD1602_DB = 0x34; //写入数字4 E = 1; delay(5); E = 0; ​ LCD1602_DB = 0x35; //写入数字5 E = 1; delay(5); E = 0; ​ delay(500); //间隔时间为500ms } }在上面的代码中,定义了函数 delay 用于延时等待,并且实现了LCD1602的初始化和写入操作。在主函数中,执行LCD1602的初始化操作,然后循环不断向LCD中写入数字12345,并且间隔时间为500ms。3.3 完整代码 #include<reg52.h> #include<intrins.h> ​ #define uchar unsigned char #define uint unsigned int ​ sbit SDA = P2^0; //定义SDA引脚 sbit SCL = P2^1; //定义SCL引脚 sbit CS = P0^3; //定义液晶屏片选引脚 sbit RW = P0^1; //定义液晶屏读/写引脚 sbit RS = P0^0; //定义液晶屏指令/数据引脚 sbit E = P0^2; //定义液晶屏使能引脚 ​ void delay(int n) //延时函数,n为延时时间 { int i; while(n--) { for(i=0; i<120; i++); } } ​ void start() //开始信号 { SDA = 1; //数据线高电平 _nop_(); SCL = 1; //时钟线高电平 _nop_(); SDA = 0; //数据线低电平 _nop_(); SCL = 0; //时钟线低电平 _nop_(); } ​ void stop() //结束信号 { SDA = 0; //数据线低电平 _nop_(); SCL = 1; //时钟线高电平 _nop_(); SDA = 1; //数据线高电平 _nop_(); } ​ void ack() //应答信号 { SDA = 0; //数据线低电平 _nop_(); SCL = 1; //时钟线高电平 _nop_(); SCL = 0; //时钟线低电平 _nop_(); SDA = 1; //数据线高电平 _nop_(); } ​ void nack() //非应答信号 { SDA = 1; //数据线高电平 _nop_(); SCL = 1; //时钟线高电平 _nop_(); SCL = 0; //时钟线低电平 _nop_(); } ​ void write_byte(uchar dat) //写一个字节 { uchar i; for(i=0; i<8; i++) { SDA = dat & 0x80; _nop_(); SCL = 1; _nop_(); SCL = 0; _nop_(); dat <<= 1; } ack(); } ​ uchar read_byte() //读一个字节 { uchar i, dat; for(i=0; i<8; i++) { dat <<= 1; SCL = 1; _nop_(); dat |= SDA; SCL = 0; _nop_(); } return dat; } ​ void init_sht30() //SHT30初始化 { start(); write_byte(0x80); if(read_byte() != 0x5A) { stop(); return; } write_byte(0xBE); if(read_byte() != 0x08 || read_byte() != 0x00) { stop(); return; } stop(); } ​ void measure() //测量温湿度值 { float humi, temp; uint i; start(); write_byte(0x80); read_byte(); read_byte(); read_byte(); write_byte(0x2C); write_byte(0x06); for(i=0; i<40000; i++); //等待测量结果 start(); write_byte(0x80); read_byte(); read_byte(); read_byte(); humi = read_byte() * 256; humi += read_byte(); temp = read_byte() * 256; temp += read_byte(); stop(); temp = -45 + (175*temp)/65535; //转化温度 humi = 100 * humi / 65535; //转化湿度 //将温湿度值通过串口发送 printf("Temperature: %.1fC\n", temp); printf("Humidity: %.1f%%RH\n", humi); } ​ void init_lcd() //液晶屏初始化 { RW = 0; RS = 0; E = 0; delay(15); write_byte(0x30); delay(15); write_byte(0x30); delay(5); write_byte(0x30); delay(5); write_byte(0x38); write_byte(0x08); write_byte(0x01); write_byte(0x06); write_byte(0x0c); } ​ void display(float temp, float humi) //显示温湿度值 { uchar i; uchar temp_str[5]; uchar humi_str[5]; //转化为字符串 sprintf(temp_str, "%.1f", temp); sprintf(humi_str, "%.1f", humi); //显示温度 RS = 0; E = 1; P1 = 0x80; //第一行第一个字符 E = 0; RS = 1; for(i=0; i<5; i++) { E = 1; P1 = temp_str[i]; E = 0; } //显示湿度 RS = 0; E = 1; P1 = 0xc0; //第二行第一个字符 E = 0; RS = 1; for(i=0; i<5; i++) { E = 1; P1 = humi_str[i]; E = 0; } } ​ void main() { init_sht30(); //SHT30初始化 init_lcd(); //液晶屏初始化 while(1) { measure(); //测量温湿度值并通过串口发送 delay(1000); display(temp, humi); //显示温湿度值 } } ​
总条数:641 到第 页
上滑加载中