• [技术干货] 3. C语言总结_数组全方位练习
    复盘C语言数组的练习题:涉及到数组插入、数组删除、数组下标数据的左移右移、数组排序、数组排序优化、数组的数据拼接等等。(1)函数基本使用: 函数基本定义、封装、传参、调用、局部变量、全局变量、只读变量、静态变量 (2)数组基本使用: 数组基本定义、使用赋值、字符串。1. 数组基本知识1. 概念: 同一个类型的集合。 2. 定义: int data[100]; //数组data里可以存放100个int类型 100必须是常量(C89)。定义数组时,如果没有赋初始值[]一定要填大小。 int data[]={1}; 3.数组赋值: 只有在定义的使用才可以整体赋值。int data[10]={12,34,56,7,89}; 4.数组下标[]访问从0开始。 比如: buff[100]; 下标范围: 0~99 5.数组的名称就是首地址。int buff[100]; 1.2 字符串数组 1. 字符串录入: scanf(“%s”); gets() fgets 2. 头文件: string.h 3. 从键盘录入字符串的方法 scanf("%s",str); //从键盘上录入字符串,字符串结尾: '\0' gets(str); //从键盘上录入字符串 fgets(str,100,stdin); //从键盘上录入字符串 (标准输入) #include int main(int argc,char **argv) { int int_data[100]={12,34,56,78}; //整型数组 char char_data[]={'A','B','C'}; //字符数组 char str1_data[]={'A','B','C','\0'}; //字符串数组 char str2_data[]="ABC";//字符串数组 printf("char_data=ß3fa3b55-b4a1-4e93-bad7-bbde26658045n",sizeof(char_data)); //3 printf("str1_data=ß3fa3b55-b4a1-4e93-bad7-bbde26658045n",sizeof(str1_data)); //4 printf("str2_data=ß3fa3b55-b4a1-4e93-bad7-bbde26658045n",sizeof(str2_data)); //4 return 0; } 字符串数组特性: 结尾有’\0’2. 数组相关的题目(分析)2.1 数组的数据插入int a[]={1,2,3,4,5,6,7}; 在第2位数据后插入一个888 结果: 1,2,888,3,4,5,6,7 #include int main(int argc,char **argv) { int data[10]={1,2,3,4,5,6,7,8,9}; //1,2,3,4,4,5,6,7,8,9 int addr; int i; printf("输入插入的位置:"); scanf("%d",&addr); /*1. 向后移动数据,空出位置*/ for(i=8;i>=addr;i--) { data[i+1]=data[i]; } /*2. 查看移动之后的效果*/ for(i=0;i<10;i++)printf("%d ",data[i]); printf("\n"); data[addr]=888; //插入的数据 /*3. 最终效果*/ for(i=0;i<10;i++)printf("%d ",data[i]); printf("\n"); return 0; }2.2 数组数据的删除int a[]={1,2,3,4,2,6,7}; 将第2位数据删除掉 结果: 1,3,4,5,6,7 示例: #include int main(int argc,char **argv) { int data[10]={1,2,3,4,5,6,7,8,9,10}; //1,2,4,5,6,7,8,9,10,10 int addr; int i; printf("输入删除的位置:"); scanf("%d",&addr); //2 /*1. 向前移动,将数据覆盖掉*/ for(i=addr-1;i<10-1;i++) { data[i]=data[i+1]; } /*2. 查看移动之后的效果*/ for(i=0;i<9;i++)printf("%d ",data[i]); printf("\n"); return 0; }2.3 变量排序数组数据排序。从键盘上输入一串数据(整数),从大到小排序,再输出。int a[]={12,67,45,13,1,5}; 排序之后: 67,45,13,12,5,1排序: 计算平均数。班级成绩、温度数据......例子: 从键盘上随机输入3个数,将3个数按照从大到小的顺序输出。#include int main(int argc,char **argv) { int a,b,c; int tmp; //存放临时数据 printf("请输入3个数:"); scanf("%d%d%d",&a,&b,&c); //以回车作为结束符号,以空格作为间隔符号 printf("源数据:%d,%d,ß3fa3b55-b4a1-4e93-bad7-bbde26658045n",a,b,c); //从大到小 if(a2.4 数组冒泡排序#include int main(int argc,char **argv) { int data[10]; int tmp; //存放临时数据 int i,j; int cnt=0; //排序比较的总次数 /*1. 录入数据*/ printf("请输入10个数:"); for(i=0;i<10;i++)scanf("%d",&data[i]); /*2. 打印源数据:*/ printf("源数据:"); for(i=0;i<10;i++)printf("%d ",data[i]); printf("\n"); /*3. 冒泡排序*/ for(i=0;i<10-1;i++) //比较轮数 { for(j=0;j<10-1-i;j++) //每一轮比较的次数 { if(data[j]>data[j+1]) //从小到大 { tmp=data[j]; data[j]=data[j+1]; data[j+1]=tmp; } cnt++; } } /*4. 打印结果数据:*/ printf("排序之后的数据:"); for(i=0;i<10;i++)printf("%d ",data[i]); printf("\n"); printf("排序的总次数:ß3fa3b55-b4a1-4e93-bad7-bbde26658045n",cnt); return 0; }分析:/* data[]={1,2,3} 第一轮: [0]和[1] [1]和[2] 2 3 1 每一轮保证最小的数在最后 第二轮: [0]和[1] 3 2 1 */ 排序: 从大到小 1 2 3 4 5 第一轮: (1) 2 1 3 4 5 (2) 2 3 1 4 5 (3) 2 3 4 1 5 (4) 2 3 4 5 1 第二轮: (1) 3 2 4 5 1 (2) 3 4 2 5 1 (3) 3 4 5 2 1 (4) 3 4 5 2 1 第三轮: (1) 4 3 5 2 1 (2) 4 5 3 2 1 (3) 4 5 3 2 1 (4) 4 5 3 2 1 第四轮: (1) 5 4 3 2 1 (2) 5 4 3 2 1 (3) 5 4 3 2 1 (4) 5 4 3 2 12.5 冒泡排序优化#include int main(int argc,char **argv) { int data[10]; int tmp; //存放临时数据 int i,j; int cnt=0; //排序比较的总次数 45: int flag=0; //标志位置 /*1. 录入数据*/ printf("请输入10个数:"); for(i=0;i<10;i++)scanf("%d",&data[i]); /*2. 打印源数据:*/ printf("源数据:"); for(i=0;i<10;i++)printf("%d ",data[i]); printf("\n"); /*3. 冒泡排序*/ for(i=0;i<10-1;i++) //比较轮数 { for(j=0;j<10-1-i;j++) //每一轮比较的次数 { if(data[j]>data[j+1]) //从小到大 { tmp=data[j]; data[j]=data[j+1]; data[j+1]=tmp; flag=1; } cnt++; } if(flag==0) //不需要继续排序 { break; //跳出最外层循环 } flag=0; //清除标志位,方便第2次判断 } /*4. 打印结果数据:*/ printf("排序之后的数据:"); for(i=0;i<10;i++)printf("%d ",data[i]); printf("\n"); printf("排序的总次数:ß3fa3b55-b4a1-4e93-bad7-bbde26658045n",cnt); return 0; }分析:data[]={1,2,3} 第一轮: [0]和[1] [1]和[2] 2 3 1 每一轮保证最小的数在最后 第二轮: [0]和[1] 3 2 12.6 数组数据的拼接(整数)int a[]={1,3,4,5,6,7}; int b[]={11,13,14,15,16,17};将a和b拼接到c数组里。 最终int c[]={1,3,4,5,6,7, 11,13,14,15,16,17} 必须保证c数组空间足够大。示例代码:#include int main(int argc,char **argv) { int src_data[10]={1,2,3,4,5}; int new_data[]={6,7,8,9}; // int i; for(i=0;i
  • [技术干货] 3. C语言总结_数组全方位练习
    复盘C语言数组的练习题:涉及到数组插入、数组删除、数组下标数据的左移右移、数组排序、数组排序优化、数组的数据拼接等等。(1)函数基本使用: 函数基本定义、封装、传参、调用、局部变量、全局变量、只读变量、静态变量 (2)数组基本使用: 数组基本定义、使用赋值、字符串。1. 数组基本知识1. 概念: 同一个类型的集合。 2. 定义: int data[100]; //数组data里可以存放100个int类型 100必须是常量(C89)。定义数组时,如果没有赋初始值[]一定要填大小。 int data[]={1}; 3.数组赋值: 只有在定义的使用才可以整体赋值。int data[10]={12,34,56,7,89}; 4.数组下标[]访问从0开始。 比如: buff[100]; 下标范围: 0~99 5.数组的名称就是首地址。int buff[100]; 1.2 字符串数组 1. 字符串录入: scanf(“%s”); gets() fgets 2. 头文件: string.h 3. 从键盘录入字符串的方法 scanf("%s",str); //从键盘上录入字符串,字符串结尾: '\0' gets(str); //从键盘上录入字符串 fgets(str,100,stdin); //从键盘上录入字符串 (标准输入) #include int main(int argc,char **argv) { int int_data[100]={12,34,56,78}; //整型数组 char char_data[]={'A','B','C'}; //字符数组 char str1_data[]={'A','B','C','\0'}; //字符串数组 char str2_data[]="ABC";//字符串数组 printf("char_data=Ö4d5b345-b7a6-4962-8973-35751b3898a3n",sizeof(char_data)); //3 printf("str1_data=Ö4d5b345-b7a6-4962-8973-35751b3898a3n",sizeof(str1_data)); //4 printf("str2_data=Ö4d5b345-b7a6-4962-8973-35751b3898a3n",sizeof(str2_data)); //4 return 0; } 字符串数组特性: 结尾有’\0’2. 数组相关的题目(分析)2.1 数组的数据插入 int a[]={1,2,3,4,5,6,7}; 在第2位数据后插入一个888 结果: 1,2,888,3,4,5,6,7 #include int main(int argc,char **argv) { int data[10]={1,2,3,4,5,6,7,8,9}; //1,2,3,4,4,5,6,7,8,9 int addr; int i; printf("输入插入的位置:"); scanf("%d",&addr); /*1. 向后移动数据,空出位置*/ for(i=8;i>=addr;i--) { data[i+1]=data[i]; } /*2. 查看移动之后的效果*/ for(i=0;i<10;i++)printf("%d ",data[i]); printf("\n"); data[addr]=888; //插入的数据 /*3. 最终效果*/ for(i=0;i<10;i++)printf("%d ",data[i]); printf("\n"); return 0; }2.2 数组数据的删除 int a[]={1,2,3,4,2,6,7}; 将第2位数据删除掉 结果: 1,3,4,5,6,7 示例: #include int main(int argc,char **argv) { int data[10]={1,2,3,4,5,6,7,8,9,10}; //1,2,4,5,6,7,8,9,10,10 int addr; int i; printf("输入删除的位置:"); scanf("%d",&addr); //2 /*1. 向前移动,将数据覆盖掉*/ for(i=addr-1;i<10-1;i++) { data[i]=data[i+1]; } /*2. 查看移动之后的效果*/ for(i=0;i<9;i++)printf("%d ",data[i]); printf("\n"); return 0; }2.3 变量排序数组数据排序。从键盘上输入一串数据(整数),从大到小排序,再输出。int a[]={12,67,45,13,1,5}; 排序之后: 67,45,13,12,5,1排序: 计算平均数。班级成绩、温度数据......例子: 从键盘上随机输入3个数,将3个数按照从大到小的顺序输出。#include int main(int argc,char **argv) { int a,b,c; int tmp; //存放临时数据 printf("请输入3个数:"); scanf("%d%d%d",&a,&b,&c); //以回车作为结束符号,以空格作为间隔符号 printf("源数据:%d,%d,Ö4d5b345-b7a6-4962-8973-35751b3898a3n",a,b,c); //从大到小 if(a2.4 数组冒泡排序#include int main(int argc,char **argv) { int data[10]; int tmp; //存放临时数据 int i,j; int cnt=0; //排序比较的总次数 /*1. 录入数据*/ printf("请输入10个数:"); for(i=0;i<10;i++)scanf("%d",&data[i]); /*2. 打印源数据:*/ printf("源数据:"); for(i=0;i<10;i++)printf("%d ",data[i]); printf("\n"); /*3. 冒泡排序*/ for(i=0;i<10-1;i++) //比较轮数 { for(j=0;j<10-1-i;j++) //每一轮比较的次数 { if(data[j]>data[j+1]) //从小到大 { tmp=data[j]; data[j]=data[j+1]; data[j+1]=tmp; } cnt++; } } /*4. 打印结果数据:*/ printf("排序之后的数据:"); for(i=0;i<10;i++)printf("%d ",data[i]); printf("\n"); printf("排序的总次数:Ö4d5b345-b7a6-4962-8973-35751b3898a3n",cnt); return 0; }分析:/* data[]={1,2,3} 第一轮: [0]和[1] [1]和[2] 2 3 1 每一轮保证最小的数在最后 第二轮: [0]和[1] 3 2 1 */ 排序: 从大到小 1 2 3 4 5 第一轮: (1) 2 1 3 4 5 (2) 2 3 1 4 5 (3) 2 3 4 1 5 (4) 2 3 4 5 1 第二轮: (1) 3 2 4 5 1 (2) 3 4 2 5 1 (3) 3 4 5 2 1 (4) 3 4 5 2 1 第三轮: (1) 4 3 5 2 1 (2) 4 5 3 2 1 (3) 4 5 3 2 1 (4) 4 5 3 2 1 第四轮: (1) 5 4 3 2 1 (2) 5 4 3 2 1 (3) 5 4 3 2 1 (4) 5 4 3 2 12.5 冒泡排序优化#include int main(int argc,char **argv) { int data[10]; int tmp; //存放临时数据 int i,j; int cnt=0; //排序比较的总次数 45: int flag=0; //标志位置 /*1. 录入数据*/ printf("请输入10个数:"); for(i=0;i<10;i++)scanf("%d",&data[i]); /*2. 打印源数据:*/ printf("源数据:"); for(i=0;i<10;i++)printf("%d ",data[i]); printf("\n"); /*3. 冒泡排序*/ for(i=0;i<10-1;i++) //比较轮数 { for(j=0;j<10-1-i;j++) //每一轮比较的次数 { if(data[j]>data[j+1]) //从小到大 { tmp=data[j]; data[j]=data[j+1]; data[j+1]=tmp; flag=1; } cnt++; } if(flag==0) //不需要继续排序 { break; //跳出最外层循环 } flag=0; //清除标志位,方便第2次判断 } /*4. 打印结果数据:*/ printf("排序之后的数据:"); for(i=0;i<10;i++)printf("%d ",data[i]); printf("\n"); printf("排序的总次数:Ö4d5b345-b7a6-4962-8973-35751b3898a3n",cnt); return 0; }分析:data[]={1,2,3} 第一轮: [0]和[1] [1]和[2] 2 3 1 每一轮保证最小的数在最后 第二轮: [0]和[1] 3 2 12.6 数组数据的拼接(整数) int a[]={1,3,4,5,6,7}; int b[]={11,13,14,15,16,17}; 将a和b拼接到c数组里。 最终int c[]={1,3,4,5,6,7, 11,13,14,15,16,17} 必须保证c数组空间足够大。示例代码:#include int main(int argc,char **argv) { int src_data[10]={1,2,3,4,5}; int new_data[]={6,7,8,9}; // int i; for(i=0;i
  • [技术干货] 2. C语言总结_函数知识
    这篇介绍C语言里函数的定义、子函数的使用。接着介绍变量的作用域,定义全局变量、静态变量、只读变量、变量初始化等等。一、函数定义//定义一个函数 int func(int a,int b) { } <函数的返回值类型> <函数名称>(函数的形参列表,....) { 函数代码主体部分; }<函数的返回值类型>: 返回值类型可以是C语言支持任何类型。基本数据类型、指针、结构体、枚举… 如果函数执行完毕,不需要返回值,可以将函数返回值类型声明为空类型。Void 函数名称 :不能与库函数名称冲突,命名规则与变量命名规则一样。 函数的形参列表 :函数在执行行传入的参数,类型与返回值类型定义方法一样。 如果有多个形参,可以使用逗号隔开 函数的返回值: 如果函数执行完毕之后需要向调用处返回数据,可以使用return,该语句只能返回一个值。#include int func(int,int); //声明func函数 //int func(int a,int b);//声明func函数 int main(void) { int a; a=func(12.34,56.78); //形参传入之后,会转为整型 printf("a=×e7c7fa4-1f36-4adb-8050-c19a00089d41n",a); return 0; } //定义一个函数 int func(int a,int b) { int c; c=a+b; return c; //给调用者返回结果 }二、函数练习作业(1)编写一个判断某一年是否是闰年的函数。(条件: 能整除4且不能整除100 或者能整除400 )#include int func_year(int year); //声明函数 int main(void) { int year; //c89标准 int err=0; printf("输入一个年份:"); scanf("%d",&year); err=func_year(year);//调用函数 if(err==1) { printf("闰年!\n"); } else if(err==0) { printf("平年!\n"); } else { printf("输入的年份错误!\n"); } return 0; }(2)该函数由主函数调用,判断平年和闰年。/* 函数功能: 判断平年和闰年 返回值 : 0表示平年,1表示闰年,负数表示错误 */ int func_year(int year) { if(year<1900)return -1; //加一个限制条件 if((year%4==0&&year0!=0)||year@0==0) { return 1; } return 0; }(3)输入一个华氏温度,输出摄氏温度,计算公式为(华氏度-32)×5÷9 要求结果保留两位小数。#include float func_temp(float temp); //声明函数 int main(void) { float temp; printf("输入一个温度值:"); scanf("%f",&temp); printf("temp=%.2f\n",func_temp(temp)); return 0; } /* 函数功能: 计算温度 返回值 : 摄氏度 */ float func_temp(float temp) { //(华氏度-32)×5÷9 return (temp-32)*5/9.0; }(4)封装函数,打印以下图案: 回文三角形,形参可以决定行数。 1 121 12321 1234321(5)计算百分比与数据自动转换#include int main(void) { float data; data=(10/60.0)*100; //运算时,需要一个数据是浮点数,运算中才可以使用浮点方式存储 printf("data=%.0f¤05db0a-6908-4dfd-ad50-33a5f3eba3b0n",data); return 0; }(6)函数返回值例子: 限定范围#include int func(int a); int main(void) { printf("×e7c7fa4-1f36-4adb-8050-c19a00089d41n",func(200)); return 0; } int func(int a) { return (a==100); //限定范围值为0和1 }三、 变量的作用域3.1 全局变量和局部变量const int c; //定义只读变量 static int b; //定义静态变量说明: 变量定义作用域分为全局变量和局部变量。 1.局部变量和全局变量名称可以相同。 2.如果局部变量名称与全局变量名称相同,优先使用局部变量。#include void func(int); int data=123; //全局变量(公用变量) int main(void) { int data=456; //局部变量 printf("data1=×e7c7fa4-1f36-4adb-8050-c19a00089d41n",data); func(666); return 0; } void func(int data) { printf("data2=×e7c7fa4-1f36-4adb-8050-c19a00089d41n",data); }3.2 只读变量#include void func(int); const int data=888; //只读变量 int main(void) { //data=666; 错误的 printf("×e7c7fa4-1f36-4adb-8050-c19a00089d41n",data); return 0; } void func(int data) { printf("data2=×e7c7fa4-1f36-4adb-8050-c19a00089d41n",data); }3.3 静态变量静态变量测试 #include int func(void); int main(void) { int i,data; for(i=0;i<5;i++) { data=func(); } printf("data=×e7c7fa4-1f36-4adb-8050-c19a00089d41n",data); return 0; } int func(void) { //int data=0; //局部变量,生命周期随着函数调用结束而终止。 static int data=0; //静态变量,生命周期与main函数一样。 //static int data=0 只有第一次执行有效 data++; //data=data+1 ,1 return data; }3.4 静态全局变量#include //int data; 全局变量,可以在其他.c文件引用 static int data=0; //静态全局变量-----局部变量 //静态全局变量: 表示该data变量不能被其他文件所引用。 //防止全局变量,重命名。 int main(void) { return 0; }3.5 静态函数#include static int func(void); int main(void) { func(); return 0; } //定义静态函数,表示该函数只能在本文件使用。 static int func(void) { printf("123\n"); }3.6 变量的初始化值#include static int data1; int data2; int main(void) { int data3; //局部变量 static int data4; printf("data1=×e7c7fa4-1f36-4adb-8050-c19a00089d41n",data1); //0 printf("data2=×e7c7fa4-1f36-4adb-8050-c19a00089d41n",data2); //0 printf("data3=×e7c7fa4-1f36-4adb-8050-c19a00089d41n",data3); //未知值 printf("data4=×e7c7fa4-1f36-4adb-8050-c19a00089d41n",data4); //0 int cnt; //cnt++; /* for(i=0;i<5;i++) { if(xxxx)data3|=0x1; data3<<=1; }*/ return 0; }
  • [技术干货] 1. C语言总结_格式化打印函数、字符串、运算符
    字符串函数的运用、sprintf、sscanf、main标准形参、变量和常量定义、基本数据占用字节数总结、scanf函数输入数据、位运算全面介绍、基本运算符全面介绍。1. 基本数据类型转字符串格式化打印: int sprintf(char *str, const char *format, ...); int snprintf(char *str, size_t size, const char *format, ...); printf默认将数据格式化打印到标准输出(文件指针-->显示终端)。 int printf(const char *format, ...); 示例: #include //标准输入输出 #include //字符串处理头文件 int main(int argc,char **argv) { char str[100]; //int data=666; //sprintf(str,"---%d---",data); //printf("%s\n",str); //float data=666.123; //sprintf(str,"---%.3f---",data); //printf("%s\n",str); //int data=12345; //sprintf(str,"---0x%X---",data); //printf("%s\n",str); sprintf(str,"%s-%s-%s-Ña84be80-b7ad-41ed-a0b2-4190975045fbn","123","456","789",888); printf("%s\n",str); return 0; }2. 字符串转基本数据类型int scanf(const char *format, ...); int fscanf(FILE *stream, const char *format, ...); int sscanf(const char *str, const char *format, ...); 示例: #include //标准输入输出 #include //字符串处理头文件 int main(int argc,char **argv) { //char str[]="123"; //int data; //sscanf(str,"%d",&data); //printf("data=Ña84be80-b7ad-41ed-a0b2-4190975045fbn",data); char str[]="123.456"; float data; sscanf(str,"%f",&data); printf("data=%.3f\n",data); return 0; }3. 数组类型当做函数的形参传递C语言的代码是由函数组成的。 函数返回值类型 函数名称(函数的形参列表,....) { ..... return <返回值的类型>; } int func(int a,int b) { return a+b; } 函数的基本运用示例: #include //标准输入输出 #include //字符串处理头文件 int sum(int a,int b); int main(int argc,char **argv) { int a=100; int b=100; int c; c=sum(a,b); //调用函数 printf("c=Ña84be80-b7ad-41ed-a0b2-4190975045fbn",c); return 0; } /* 函数功能: 求和 */ int sum(int a,int b) { return a+b; }4. 数组当做函数形参类型数组类型可以当做函数的形参,但是数组类型不能当做函数返回值类型(只能通过指针返回)。示例(通过子函数给主函数里的数组赋值):#include //标准输入输出 #include //字符串处理头文件 void func(char str[]); //函数声明 int main(int argc,char **argv) { char str[100]; func(str); printf("%s\n",str); return 0; } //数组当做函数形参时,填入的数组的首地址 void func(char str[]) { scanf("%s",str); }封装子函数:示例(封装排序的子函数): #include //标准输入输出 #include //字符串处理头文件 void func(int data[],int len); //函数声明 int main(int argc,char **argv) { int data[10]={5,4,3,2,1}; int i; func(data,5); for(i=0;i<5;i++) { printf("%d ",data[i]); } printf("\n"); return 0; } //数组当做函数形参时,填入的数组的首地址 void func(int data[],int len) { int i,j; int tmp; for(i=0;idata[j+1]) { tmp=data[j]; data[j]=data[j+1]; data[j+1]=tmp; } } } }5. C语言main函数标准形参#include /*引用系统的库函数*/ /* main: 主函数表示开头 函数形参介绍: int argc :传入的参数个数 char **argv:传入的参数缓冲区地址。 参数示例: ./app 123 456 789 注意点: 传入的任何参数都是以字符串的形式存放的。 */ int main(int argc,char **argv) { int i; printf("argc=Ña84be80-b7ad-41ed-a0b2-4190975045fbn",argc); for(i=0;i6. 变量和常量#include /*引用系统的库函数*/ int main(int argc,char **argv) { int a; /*定义一个整型的变量a *什么是变量? (变量)空间数据可以改变。 *什么是常量? 空间的数据初始化之后不能改变。 */ const int b=123; //定义一个整型的常量b,并初始化赋值为123 return 0; }7. 基本的数据类型#include /*引用系统的库函数*/ int main(int argc,char **argv) { int a=12; //整型变量、占4个字节。 printf("sizeof=%d,Ña84be80-b7ad-41ed-a0b2-4190975045fbn",sizeof(int),a); char b='A'; //字符类型变量、占1个字节。 printf("sizeof=%d,Áa84be80-b7ad-41ed-a0b2-4190975045fbn",sizeof(char),b); float c=12.1; //浮点类型变量、占4个字节。 printf("sizeof=%d,%.2f\n",sizeof(float),c); double d=12.1; //双精度类型变量、占8个字节。 printf("sizeof=%d,%.3f\n",sizeof(double),d); long e=12; //长整型、占4个字节。 printf("sizeof=%d,%ld\n",sizeof(long),e); long long f=12; //双长整型、占8个字节。 printf("sizeof=%d,%lld\n",sizeof(long long),f); short 短整型 占2个字节 /* * 1字节多大?8个位 ,8个位最大可以存放多少数据? 255->0xFF * */ unsigned int a_1=12; //定义无符号整型。 /* * unsigned关键字只能使用在整型上。 */ return 0; }8. char类型变量#include /*引用系统的库函数*/ int main(int argc,char **argv) { char a=128; printf("a=Ña84be80-b7ad-41ed-a0b2-4190975045fbn",a); /* * unsigned char a; 存放数据的范围: 0~255 * char a;存放的范围: -128 ~ +127 */ /* 常用的变量ASCII码。 在电脑上的所有的数据在硬盘上存放都是以二进制形式存放。 二进制-->转10进制。 */ return 0; }9. scanf函数使用#include /*引用系统的库函数*/ int main(int argc,char **argv) { int a,b; /*a这个名称在当前工程里不能重复、整个操作系统是可以重复*/ scanf("%d%d",&a,&b); /* &表示取地址 * 从键盘上读取一个整型的a给变量。 */ /* scanf函数带阻塞功能: 当遇到回车才会结束输入。空格可以当做间隔符号。 */ printf("data=%d,Ña84be80-b7ad-41ed-a0b2-4190975045fbn",a,b); return 0; }10. 运算符基本算术运算符 + :加法运算符 - :减法运算符 *:乘法运算符 / :除法运算符 %:取余运算符 = :赋值运算符。 右边值给左边值。 c=(a=b) , c=(a==b) ++ :自增,i++ 相当于i=i+1; 逻辑运算符 > 大于 、>= < 小于、 <= == 等于判断 && 逻辑与 所有条件全部为真,结果才为1 || 逻辑或 只要有一个条件为真,结果就为1 ! 逻辑非 。非0即为0。 11. 位运算位运算: 针对二进制数字进行的,0和1。 & 按位与。 比如: a=a&0x01; 规则: 全1为1,有0为0 #include int main(int argc,char **argv) { unsigned char a=0x2; unsigned char b=0x3; a=a&b; printf("0x%X\n",a); /* 0000 0010 0000 0011 ------------- 0000 0010 */ return 0; } | 按位或。 比如: a=a|0x01; 规则: 有1为1,全0为0 #include int main(int argc,char **argv) { unsigned char a=0x2; unsigned char b=0x3; a=a|b; printf("0x%X\n",a); /* 0000 0010 0000 0011 ------------- 0000 0011 */ return 0; } ^ 按位异或。 规则: 相同为0,不同为1 #include int main(int argc,char **argv) { unsigned char a=0x2; unsigned char b=0x3; a=a^b; printf("0x%X\n",a); //1 /* 0000 0010 0000 0011 ------------- 0000 0001 */ return 0; } ~ 按位取反。 规则: 0为1,1为0 #include int main(int argc,char **argv) { unsigned char a=0x7F; a=~a; printf("0x%X\n",a); /* 01111111 ---------- 10000000 */ return 0; } >> 右移运算符。规则: 低位溢出,高位补0 #include int main(int argc,char **argv) { unsigned char a=0xF1; a=a>>1; printf("0x%X\n",a); /* 11110001 >>0111 1000 0x78 */ return 0; } << 左移运算符。规则: 高位溢出,低位补0 #include int main(int argc,char **argv) { unsigned char a=0x1F; a=a<<1; printf("0x%X\n",a); /* 0001 1111 ------------- 0011 1110 */ return 0; }
  • [技术干货] 嵌入式物联网开发-案例整理集合
    一、前言物联网是互联网基础上的延伸和扩展的网络,将各种信息传感设备与互联网结合起来而形成的一个巨大网络,实现在任何时间、任何地点,人、机、物的互联互通。物联网的底层是感知层,感知层主要器件是传感器,作用是使用传感器收集信息;收集到的信息会发给传输层,传输层的核心的无线网络(WiFi,蓝牙,zigbee等),作用是将感知层收集的信息传输给上层应用层,应用层是所谓的云服务器。应用层通过大数据,云计算等手段最终得出结论,在通过传输层发出操作指令给底层去执行。单片机是物联网感知层的核心,而接在单片机上的各种传感器就是单片机的感官,通过这些传感器采集数据,经过单片机统一处理传递给应用终端进行统一分析,完成互联互通。这篇文章合集就列出了单片机的常用传感器开发案例、IIC协议、SPI协议、上位机开发、红外线时序解码、编码。在STM32上完成IIC模拟协议、IIC硬件时序实现,SPI模拟协议,SPI硬件实习的实现思路,利用AT24C02、W25Q64介绍IIC时序、SPI时序。二、开发案例2.1 STM32入门开发 介绍IIC总线、读写AT24C02(EEPROM)(采用模拟时序)连接:https://bbs.huaweicloud.com/forum/thread-195865-1-1.html介绍IIC时序,采用寄存器方式开发 (方便程序移植到其他单片机),STM32本身支持IIC硬件时序的,本文采用的是模拟时序,下篇文章就介绍配置STM32的IIC硬件时序读写AT24C02和AT24C08。模拟时序更加方便移植到其他单片机,通用性更高,不分MCU;硬件时序效率更高,单每个MCU配置方法不同,依赖硬件本身支持。AT24C02 是串行CMOS类型的EEPROM存储芯片,AT24C0x这个系列包含了AT24C01、AT24C02、AT24C04、AT24C08、AT24C16这些具体的芯片型号。他们容量分别是:1K (128 x 8)、2K (256 x 8)、4K (512 x 8)、8K (1024 x 8)、16K (2048 x 8) ,其中的8表示8位(bit)。AT24C02系列支持I2C,总线数据传送协议I2C,总线协议规定任何将数据传送到总线的器件作为发送器。任何从总线接收数据的器件为接收器;数据传送是由产生串行时钟和所有起始停止信号的主器件控制的。主器件和从器件都可以作为发送器或接收器,但由主器件控制传送数据(发送或接收)的模式,由于A0、A1和A2可以组成000~111八种情况,即通过器件地址输入端A0、A1和A2可以实现将最多8个AT24C02器件连接到总线上,通过进行不同的配置进行选择器件。2.2 基于CC2530_ZigBee+华为云IOT设计的冷链信息采集系统 连接:https://bbs.huaweicloud.com/forum/thread-195953-1-1.html这篇文章以CC2530单片机为核心器件,设计一个冷链环境信息采集系统,利用传感器技术对冷藏仓内的环境参数进行采集,上传到物联网云平台,然后通过手机端或移动端进行显示,便于分析,观察冷链环境信息。当前的设计中,用的物联网平台服务是华为云的设备接入服务(IoTDA),IoTDA提供海量设备连接上云、设备和云端双向消息通信、批量设备管理、远程控制和监控、OTA升级、设备联动规则等能力,并可将设备数据灵活流转到华为云其他服务。使用物联网平台构建一个完整的物联网解决方案主要包括3部分:物联网平台、业务应用和设备。物联网平台作为连接业务应用和设备的中间层,屏蔽了各种复杂的设备接口,实现设备的快速接入;同时提供强大的开放能力,支撑行业用户构建各种物联网解决方案。 设备可以通过固网、2G/3G/4G/5G、NB-IoT、Wifi等多种网络接入物联网平台,并使用LWM2M/CoAP、MQTT、HTTPS协议将业务数据上报到平台,平台也可以将控制命令下发给设备。 业务应用通过调用物联网平台提供的API,实现设备数据采集、命令下发、设备管理等业务场景。2.3 STM32入门开发 介绍SPI总线、读写W25Q64(FLASH)(硬件+模拟时序) 连接:https://bbs.huaweicloud.com/forum/thread-196050-1-1.htmlW25Q64是为系统提供一个最小空间、最少引脚,最低功耗的串行Flash存储器,25Q系列比普通的串行Flash存储器更灵活,性能更优越。W25Q64支持双倍/四倍的SPI,可以储存包括声音、文本、图片和其他数据;芯片支持的工作电压 2.7V 到 3.6V,正常工作时电流小于5mA,掉电时低于1uA,所有芯片提供标准的封装。STM32本身支持SPI硬件时序,本文示例代码里同时采用模拟时序和硬件时序两种方式读写W25Q64。模拟时序更加方便移植到其他单片机,更加方便学习理解SPI时序,通用性更高,不分MCU;硬件时序效率更高,每个MCU配置方法不同,依赖MCU硬件本身支持。采用华邦W25Q64 flash存储芯片。W25Q64这类似的Flash存储芯片在单片机里、嵌入式系统里还是比较常见,可以用来存储图片数据、字库数据、音频数据、保存设备运行日志文件等。2.5 STM32入门开发 制作红外线遥控器(智能居家-万能遥控器) 连接:https://bbs.huaweicloud.com/forum/thread-196145-1-1.html红外线协议有很多,本章节主要是针对NEC协议讲解,只要把NEC协议原理搞懂了,其他协议都是一样的使用;如果想要模拟空调遥控器,去控制美的空调、格力空调这些设备,就需要按照美的、格力空调的协议发送;如果不知道协议长什么样,可以将逻辑分析仪插在红外线接收头的引脚上,拿个正常的空调遥控器对着接收头按一下,然后采集数据分析,即可得到协议规律,然后网络上也有空调按键值功能的说明文档,调试一下即可。因为要模拟红外线遥控器,就需要一个红外线发射管;在学习阶段,如果不想自己搭建电路,可以买现成的模块。 买模块连接也是比较稳定,接线也比较简单,VCC和GND接好之后,把DAT引脚接到STM32任意一个IO口上即可,如果想用硬件PWM控制发送,那么引脚接到STM32的PWM输出脚即可。2.6 STM32入门开发:编写XPT2046电阻触摸屏驱动(模拟SPI) 连接:https://bbs.huaweicloud.com/forum/thread-196257-1-1.htmlXPT2046是一颗12位的ADC芯片,可以当做普通的ADC芯片使用,但是一般都是用在电阻触摸屏上,方便定位触摸屏坐标。 这篇文章介绍XPT2046芯片使用方法,介绍内部的时序,指令,使用场景,利用STM32读取XPT2046采集的触摸屏坐标。(XPT2046支持笔中断输出--低电平有效,这个引脚可以配置到单片机的中断脚上,或者轮询判断这个引脚状态,判断触摸屏是否已经按下)可以单独买一个触摸屏+一个XPT2046就可以自己做手画板、触摸按键(自己用一张纸在下面画个模型就行)、等等很多小玩意。XPT2046 数据接口是串行接口,处理器和转换器之间的通信需要 8 个时钟周期,可采用 SPI、 SSI 和 Microwire 等同步串行接口。一次完整的转换需要 24 个串行同步时钟(DCLK)来完成。2.7 基于STM32设计的环境检测设备 连接:https://bbs.huaweicloud.com/forum/thread-196528-1-1.html随着人们生活质量的提高,对于生活环境的问题,人们的关注度进一步提高,同时政府部门采取了许多措施来改善环境状况。但是总体上来说我国的环境监测技术水平比较落后,传统上的监测手段比较单一,监测数据也不够准确,耗尽了大量的人力和财力,却成效不高。针对上述缺点,当前文章综合了嵌入式处理技术、传感器技术、无线网络通信等技术,设计了一个基于STM32的无线环境监测系统,系统主要实现了对湿度、温度、有毒气体、烟雾浓度、空气质量等参数进行实时监测的功能。为了实现无线数据传输功能,采用了无线wifi技术。系统的测试分析表明系统整体数据采集性能良好,数据传输稳定性可靠,到达了预期目标。系统与传统的监测技术相比,具有监测数据准确,监测范围广,智能化高等特点。且系统具有一定的创新性,在实际的工程运用和理论研究上体现出了一定的研究价值最后通过实物的调试,各项参数及功能符合设计要求,能达到预期的目的。设计以STM32微控制器为平台,采用DHT11温湿度传感器、烟雾传感器MQ-2、易燃气体传感器MQ-4、空气质量检测传感器MQ-135对室内温湿度和危险气体进行采集。通过wifi无线网络将数据传送给微控制器,STM32微控制器处理数据后,由自带oled液晶屏显示。当室内温度达到预警值或有危险气体时,系统将会自动警报并将警报信息通过wifi网络传输给客户手机。且每隔一段时间会通过wifi自动发送监测信息到手机,从而实现对室内环境的监测及报警功能。2.8 基于CC2530(ZigBee)设计的景观照明控制系统+配套手机APP 连接:https://bbs.huaweicloud.com/forum/thread-0205957663433320004-1-1.html这是基于CC2530设计的景观照明控制系统,一共包含了3个CC2530节点(就是3块CC2530开发板)。下面将这个3个CC2530开发板称为A、B、C节点。A节点: 当做协调器、可以接收BC节点上传的数据;A模块配了一个ESP8266 WIFI模块,可以连接手机APP,将BC节点上传温湿度数据再上传给手机APP显示。如果A节点在一定时间内没有收到B、C节点的数据,就会通知手机APP,告诉用户,B、C节点已经掉线。B节点: 作为RGB多彩灯+温湿度检测节点,会根据当前温湿度调整当前RGB灯的颜色,用于告诉景区的游客,当前景区的温湿度情况,采集的温湿度也会通过CC2530传递给A节点。C节点: 光照强度检测+LED灯节点。 这里的LED灯就是模拟景区的路灯,手机APP可以控制LED灯的开关,如果是白天的时候,LED灯会自动关掉,天气变暗,自动打开。也可以设计时间,定时关灯。2.9 基于STM32的录音机设计(STM32F103+VS1053B) 连接:https://bbs.huaweicloud.com/forum/thread-0202960838554260004-1-1.html这是基于STM32F103C8T6设计的录音机功能,支持的功能如下:(1). 按下按键1启动自动录音,默认为5秒录音一次,录音完毕自动保存在SD指定目录下。文件名称采用当前时间命名;音频文件格式采用WAV格式存储。(2). 按下按键2启动手动录音,按键按下之后开始录音,再次按下结束录音,录音完毕之后,文件也是一样的保存在SD卡里。(3). SD卡文件系统采用FAT32格式,STM32移植了FATFS开源文件系统对SD卡进行读写操作。(4). OLED显示屏用于显示当前录音机的状态: 空闲、录音、回放等状态。(5). 按下按键3,启动自动回放功能。自动扫描目录,按顺序播放录音文件。2.10 基于STM32设计的拼图小游戏 连接:https://bbs.huaweicloud.com/forum/thread-0202960843099540005-1-1.html拼图游戏在小时候还是经常玩,玩法也比较简单,这里就使用STM32设计一个拼图小游戏分享出来一起学习。目前游戏是通过开发板上4按键进行控制图片的移动,完成最终的拼图效果,如果想要在上面扩充其他游戏功能都还是比较容易。当然,设计编写这个款小游戏不仅仅是为了玩游戏这么简单,主要是为了通过设计游戏的过程学习STM32编程、LCD的编程、按键、图片等等很多编程知识。做拼图这款游戏主要的硬件就是LCD显示屏和几个按键,没有其他外设,如果自己有其他STM32最小系统板有LCD显示屏的话,完全可以把代码移植过去运行。当前的代码兼容正点原子战舰开发板,可以直接在正点原子战舰直接编译下载运行。其他的开发板上运行也是一样的道理,底层换成自己LCD画点函数即可。目前的拼图的游戏工程里图片是提前通过取模软件取好模之后把数据放在工程里的,如果想要动态更加灵活的切换拼图的图片,可以加上SD卡和FATFS文件系统,把图片放在SD卡,通过按键选择指定图片切割进行拼图。2.11 基于STM32设计的实时心率检测仪 连接:https://bbs.huaweicloud.com/forum/thread-0231972099436290193-1-1.htmlPulseSensor 是一款用于脉搏心率测量的光电反射式模拟传感器。可以将其佩戴于手指、耳垂、手腕等处,通过杜邦线--导线将引脚连接到单片机,可将采集到的模拟信号传输给单片机,单片机配置ADC用来转换为数字信号,再通过单片机简单计算后就可以得到心率数值;为了方便联动健康管理系统,也方便自己了解自己的心率,可将脉搏波形通过串口、WIFI等方式上传到电脑、手机显示波形,然后根据提前配置的参数,结合算法确定是否正常。PulseSensor 是一款开源硬件, 目前国外官网上已有其对应的单片机程序,也附带有对应的上位机Processing 程序, 比较适用于心率方面的科学研究和教学演示,也非常适合用于二次开发;上位机也可以自己开发,根据自己的需求定制,达到自己想要的功能。STM32的采集代码比较简单,因为就只需要配置对应引脚的ADC功能采集即可。可以采集10次,去掉最大值最小值取平均值,拿到最终结果再传递给上位机显示。2.12 基于STM32设计的小说阅读器(翻页、字体切换、颜色切换、语音播报) 连接:https://bbs.huaweicloud.com/forum/thread-0236972110563920200-1-1.html这是基于ST32F103ZET6设计的小说阅读器,虽然对于真实的小说阅读器产品来讲,实用性和功能方面还差很多,但是对于刚入门的STM32、单片机开发工程师来讲,这里面设计到的技术才是最有价值的。所以这篇文章的小说阅读器主要是用来作为嵌入式单片机工程师入门练手项目、大学生的课程设计等。目的不在于小说阅读器,而是以小说阅读器为例子,学习相关的技术: SD卡、串口通信、SPI通信、8080时序、触摸屏校准原理、FATFS文件系统使用、语音播报模块使用等等。该阅读器支持常规阅小说读器具备的基本功能:(1). 支持选择指定的小说进行查看阅读,可以通过触摸屏上的按钮进行切换。(2). 支持切换字体大小(3). 支持切换字体颜色、背景颜色(4). 标题栏显示当前阅读器查看的小说文件名称(5). 支持翻页、上一页、下一页(6). 支持语音自动阅读,发声接近正常真人发声,非常强大。语音方案可以选择两种: (1). 宇音SYN6658 (2). 科大讯飞SYN5152。 这两款芯片都是通过串口通信,编程十分简单。2.13 基于STM32设计的校园一卡通项目-详解原理 连接:https://bbs.huaweicloud.com/forum/thread-0248972114388090177-1-1.html校园一卡通是基于智能卡物联网技术和计算机网络的数字化理念融合于校园管理进行的统一身份认证、人事、学工等MIS应用系统的应用解决方案。通过共同的身份认证机制,实现数据管理的集成与共享,使校园一卡通系统成为校园信息化建设有机的组成部分。通过这样的有机结合,可以避免重复投入,加快建设进度,为系统间的资源共享打下基础。常用的IC卡一般是M1卡,也称为S50卡,购买RC522刷卡模块送的白卡,蓝色钥匙扣、公交卡、地铁卡都是S50卡。S50卡内部有16个分区,每分区有AB两组密码,总容量为8Kbit。第0个扇区第0块用于存放厂商代码,意见固话,不可更改。每个扇区的块0、块1、块2为数据块,可以用于存储数据。数据块可以进行读写操作。每个扇区的块3为控制块,包括了密码A、存储控制、密码B。2.14 基于STM32设计的遥控小车(手机APP+GPS+温湿度+ESP8266) 连接:https://bbs.huaweicloud.com/forum/thread-0239972118311640194-1-1.html这是基于STM32设计的4轮遥控小车,支持通过Android手机APP、Windows上位机完成对小车遥控;支持前进、后退、左转、右转、停止等操作。小车上会实时采集温度、湿度、GPS经纬度、通过ESP8266 WIFI上传至手机APP,手机APP收到数据之后,会将温湿度实时显示出来,经纬度收到后会调用百度ditu,显示小车的位置,并且数据也会存放到数据库里,方便查看历史数据;支持范围内温湿度查询、最高温湿度、最低温湿度查询。小车电机驱动模块采用L298N、WIFI模块采用ESP8266、MCU采用STM32F103C8T6、温湿度模块采用DTH11、GPS模块采用北斗GPS+BDS。STM32程序风格: 采用寄存器方式开发,注释齐全,执行效率高,方便移植手机APP: 采用QT设计,程序支持跨平台编译运行(Android、IOS、Windows、Linux都可以编译运行,对应平台上QT的环境搭建,之前博客已经发了文章讲解)硬件包含: 淘宝购买的完整一套4轮遥控小车(采用STM32F103ZET6作为主控板)、DHT11温湿度传感器、中科微GPS模块、ESP8266
  • [技术干货] 基于STM32设计的掌上游戏机(运行NES游戏模拟器)详细开发过程
    一、环境与硬件介绍开发环境: keil5代码风格: 寄存器风格,没有采用库函数,底层代码全部寄存器方式编写,运行效率高,注释清楚。MCU型号: STM32F103ZET6开发板: 正常的一块STM32开发板,带LCD插槽,带4颗独立按键。游戏模拟器: NES游戏模拟器LCD : ALIENTEK的3.5寸屏幕。(屏幕型号不重要,随便一款都可以的,把屏幕底层驱动代码写好,适配即可)声音输出设备 : 采用VS1053 (SPI接口,操作方便)游戏手柄: 支持FC游戏手柄完成这个掌上游戏机需要使用的硬件设备不复杂,如果想要体验游戏,需要的必备硬件:1. (必要) STM32F103系列最小系统版一个2. (必要) LCD屏一块。 2.8寸就可以了,价格便宜。3. (非必要) FC游戏手柄一个,驱动时序很简单(后面有单独章节介绍),支持组合键,玩游戏体验感非常好。如果不用FC游戏手柄,使用开发板几个独立按键也行,只是手感不好。4. (非必要) VS1053或者其他系列声卡模块一个,游戏是有声音的,要完美的体验游戏声卡肯定是要的,不要也可以玩,只是没有声音而已。VS1053模块支持SPI接口控制,时序简单,驱动代码也不复杂,资料比较多,学起来,理解起来很容易。5. (非必要) SD卡一张。主要存储NES游戏文件,可以动态加载想要玩的游戏,切换比较方便。如果没有SD卡,也想体验也可以,直接把游戏取模成二进制放在数组里存放到STM32的FLASH里即可,STM32F103ZET6有512K的FLASH,存放一个游戏完全够用,加载速度更加快。6. (非必要) SRAM外部扩展内存,如果不需要从SD里加载游戏,就不需要外部内存;如果使用SD卡加载游戏,就需要把游戏数据从SD卡里读取出来,然后放在SRAM外部扩展内存芯片里。因为STM32F103ZET6本身只有64K内存,放不下。游戏体验: STM32可以超频到128M,运行起来还是非常流畅,玩起来的感觉和正常的FC游戏机是一样的,没有卡顿,延迟。游戏模拟器移植的是NES模拟器,开发过程中,代码编写了3个版本:版本1: 精简版的掌上游戏机,最适合学习,代码牵扯很少,只有外设硬件只用到了LCD而已,最适合学习,理解代码运行原理;不支持声音输出,不支持FC游戏手柄,不支持SD卡和文件系统(也就是不支持从SD卡上选择游戏加载)。 这个版本的游戏是直接使用数组存放在代码里的,游戏的操作是通过开发板上的4个按键控制(开发板的4个按键,分别控制角色的前进、后退、暂停、跳跃),因为只有4个按键,没有支持组合按键,所以体验起来不是很舒服,控制比较困难,完美体验还是要继续加上FC游戏手柄。版本2: 这也是精简版的掌上游戏机,在版本1的基础之上加了VS1053模块,支持声音输出,体验感要好一点,能听到游戏声音。版本3: 这是完整版本的掌上游戏机,加入了FC游戏手柄支持,加入了VS1053声卡驱动,加入了SD卡和FATFS文件系统,可以正常从SD卡加载指定的游戏运行,体验非常好。3个版本的源代码和NES的游戏集合,在下面的第3章有下载地址。二、游戏运行效果(超级玛丽示例)2.1 超级玛丽运行截图2.2 仅仅使用独立按键操作游戏效果单手录制,单手操作,操作起来起来不太方便。STM32上移植NES游戏框架-运行超级玛丽游戏2.3 游戏自动待机运行效果(没有操作)基于STM32移植NES游戏框架-超级玛丽游戏(动画)三、资料下载地址3.1 NES游戏集合下载一共有293款游戏,总有一款适合你。常见的超级玛丽、魂斗罗、都有包含的。3.2 工程源码下载一共3个版本,它们之间的区别在第一章已经介绍过。三个都是keil工程,下载下来直接编译、下载运行体验。四、什么是NES ?NES就是红白机的游戏,所谓的NES意思是欧美版的红白机,FC的美版,Nintendo entertainment system(任天堂娱乐系统),而日本的红白机则叫family computer(FC)。发展历史-来至百度百科 1983年7月15日,由日本任天堂株式会社(原本是生产日式扑克即“花札”)的宫本茂先生领导开发的一种第三代家用电子游戏机:FC,全称:Family Computer,也称作:Famicom;在日本以外的地区发售时则被称为NES,全称:Nintendo Entertainment System;在中国大陆、台湾和香港等地,因其外壳为红白两色,所以人们俗称其为“红白机”,正式进入市场销售,并于后来取得了巨大成功,由此揭开了家用电子游戏机遍布世界任何角落,电子游戏全球大普及的序幕。1985年,NES在北美地区的销量3300万台,比日本地区高出近一倍, 也占据了其全球市场份额的一半。 NES在北美首发时的捆绑游戏《打鸭子》(Duck hunt)总共取得近3000万套(基本全部来自北美市场)销量, [6] 这在红白机游戏中名列第二,仅次于《超级马力欧》。1986年,任天堂在美国收3.1亿美元,这一年美国游戏产业的规模4.3亿美元,而在一年前,深陷雅达利冲击的美国游戏业的收入仅1亿美元。 [7] 1988年发售的《超级马力欧兄弟3》(Super Mario Bros. 3)在美国售出700万套,在日本销量达400万,销售额5.5亿美元。1989年,任天堂的游戏机已占领美国90%和日本95%的市场,任天堂成为游戏界巨无霸。2003年7月,FC发售二十周年,任天堂宣布FC游戏机正式停产。至此,FC全世界已累计销售6000万部以上。至今中国大陆、台湾、香港与泰国甚至日本等地仍然在制造FC规格的兼容品。任天堂成为了现代游戏产业的开创者,在很多方面上确立了现代电子游戏的标准。 FC巨大成功使任天堂年纯利从1985年开始一直保持5亿美元以上 ,其股票成为东京证券交易所绩优股代名词,一度超越了3万日元,市值超松下等企业,很多人都把任天堂成功誉为新时代商业神话。 任天堂红白机(FC/NES)发行于1983年,在日本发行之后引起了不小的轰动,两年之后进军北美市场,更加奠定了任天堂的家用游戏机霸主地位。当人们正需要一个高品质的家用游戏机的时候,任天堂拿出了他们的全部家当,首发的数款游戏都赢得了玩家的赞誉,超级马力欧更成为了永远的经典。在那个年代,拥有一台红白机应该是孩子们最大的梦想了。 根据外媒的数据,在1990年30%的美国家庭都拥有NES主机。五、工程源码分析: 以精简版本(1)为例工程源码全部采用寄存器代码风格,基本上每行都有详细的注释;虽然STM32支持库函数方式开发,效率更加快,但是寄存器方式可以更方便了解CPU底层寄存器的一些配置,对以后在学习使用其他类型的微处理器是非常有帮助的。5.1 工程文件布局5.2 主函数代码主函数里完成LCD屏幕初始化,按键初始化,LED灯初始化,串口初始化,FC游戏手柄初始化,默认把LCD屏幕清屏为黑色。LCD屏采用FSMC驱动的,把FSMC时序速度配置到最快,达到STM32能支持的最快速度,提高LCD刷屏速度。初始化完毕最后,调用了LoadNes函数,完成游戏加载;如果加载失败,就回到下面执行while循环,闪烁LED灯。代码如下:#include "stm32f10x.h" #include "led.h" #include "lcd.h" #include "delay.h" #include "key.h" #include "usart.h" #include #include #include "joypad.h" extern u8 LoadNes(u8* pname,u32); //游戏文件可以通过winhex文件生成C源码数组 extern const unsigned char nes_data1[40976];//超级玛丽游戏的文件 extern const unsigned char nes_data2[262160];//魂斗罗游戏的文件 /* 移植说明: 1. 加入游戏手柄 2. 优化了游戏刷新的帧率 3. 加入开发板本身自带按键控制 */ int main() { BeepInit(); //蜂鸣器初始化 LedInit(); //LED灯初始化 UsartInit(USART1,72,115200); KeyInit(); //按键初始化 printf("串口工作正常!\r\n"); LcdInit(); //LCD初始化 //JoypadInit(); //游戏手柄初始化 LcdClear(0xFFFF); /* 0000 0000:保留 0000 0001: DATAST保持时间=2个HCLK时钟周期 0000 0010: DATAST保持时间=3个HCLK时钟周期 …… 1111 1111: DATAST保持时间=256个HCLK时钟周期(这是复位后的默认数值) 0、1、2、3、4、5、6、7、8、9、10、11、12、13、14 */ LcdClear(0); //开始运行游戏 LoadNes((unsigned char*)nes_data1,40976); //超级玛丽 //LoadNes((unsigned char*)nes_data2,262160); //魂斗罗 while(1) { LED1=!LED1; DelayMs(400); } }5.3 加载NES游戏:LoadNes函数介绍LoadNes函数原型:u8 LoadNes(unsigned char* pname,u32 size)该函数传入NES游戏数据地址,和游戏数据大小进来。现在这个版本没有使用SD卡和文件系统,游戏的文件数据是直接加到代码里编译的。这两个数组是超级玛丽和魂斗罗的数据。(直接使用打开文件,使用WinHEX软件打开,全选,右键编辑,选择复制,选择C源码,复制成数组形式粘贴到keil里即可)函数里面主要完成了NES模拟器基本的初始化。主要完成了STM32超频配置,配置锁相环为16倍,超频到128MHZ。超频配置代码如下:/* 函数功能:频率设置 参 数:PLL,倍频数 */ void NesClockSet(u8 PLL) { u8 temp=0; RCC->CFGR&=0XFFFFFFFC; //修改时钟频率为内部8M RCC->CR&=~0x01000000; //PLLOFF RCC->CFGR&=~(0XF<<18); //清空原来的设置 PLL-=2; //抵消2个单位 RCC->CFGR|=PLL<<18; //设置PLL值 2~16 RCC->CFGR|=1<<16; //PLLSRC ON FLASH->ACR|=0x12; //FLASH 2个延时周期 RCC->CR|=0x01000000; //PLLON while(!(RCC->CR>>25)); //等待PLL锁定 RCC->CFGR|=0x02; //PLL作为系统时钟 while(temp!=0x02) //等待PLL作为系统时钟设置成功 { temp=RCC->CFGR>>2; temp&=0x03; } } 接下来初始化NES游戏模拟器的必要参数,最后调用NesEmulateFrame函数进入NES游戏主循环代码,开始运行游戏。LoadNes函数完整代码如下:/* 函数功能:开始nes游戏 参 数:pname:nes游戏路径 u32 size 游戏大小 返 回 值: 0,正常退出 1,内存错误 2,文件错误 3,不支持的map */ u8 LoadNes(unsigned char* pname,u32 size) { u8 res=0; res=NesSramMalloc(); //申请内存 romfile=(u8*)pname; //游戏源码地址 NESrom_crc32=get_crc32(romfile+16,size-16);//获取CRC32的值 res=LoadNesRom(); //加载ROM printf("res=Ó494e7f1-4e15-445b-a118-824064c956b7r\n",res); NesClockSet(16); //设置系统时钟为128MHZ 16*8 JoypadInit(); //游戏手柄初始化 cpu6502_init(); //初始化6502,并复位 Mapper_Init(); //map初始化 PPU_reset(); //ppu复位 apu_init(); //apu初始化 NesEmulateFrame(); //进入NES模拟器主循环 return res; } 5.3 NES游戏主循环代码详细代码如下://nes模拟器主循环 void NesEmulateFrame(void) { u8 nes_frame; NesSetWindow();//设置窗口 while(1) { // LINES 0-239 PPU_start_frame(); for(NES_scanline = 0; NES_scanline< 240; NES_scanline++) { run6502(113*256); NES_Mapper->HSync(NES_scanline); //扫描一行 if(nes_frame==0)scanline_draw(NES_scanline); else do_scanline_and_dont_draw(NES_scanline); } NES_scanline=240; run6502(113*256);//运行1线 NES_Mapper->HSync(NES_scanline); start_vblank(); if(NMI_enabled()) { cpunmi=1; run6502(7*256);//运行中断 } NES_Mapper->VSync(); // LINES 242-261 for(NES_scanline=241;NES_scanline<262;NES_scanline++) { run6502(113*256); NES_Mapper->HSync(NES_scanline); } end_vblank(); NesGetGamepadval(); //每3帧读取游戏手柄数据 nes_frame++; if(nes_frame>NES_SKIP_FRAME) { nes_frame=0;//跳帧 } } }进来就先调用了**NesSetWindow(void)**函数,设置窗口大小,这里面就调用了LCD的接口,如果是其他的LCD屏,使用本代码只需要把这里适配一下即可。u8 nes_xoff=0; //显示在x轴方向的偏移量(实际显示宽度=256-2*nes_xoff) //设置游戏显示窗口 void NesSetWindow(void) { u16 lcdwidth,lcdheight; lcdwidth=256; lcdheight=240; nes_xoff=0; LcdSetWindow(32,0,lcdwidth,lcdheight); LcdWriteRAM_Prepare();//写入LCD RAM的准备 }接下来就进入到NES游戏的主循环代码,开始循环一帧一帧的刷出图像数据,达到游戏的效果。设置窗口大小之后,下面就是从NES游戏数据文件里取出颜色数据,然后for循环一行一行刷屏即可。上面的设置窗口大小的代码其实并不是必要的,只是当前使用的LCD支持坐标自增(一般LCD都支持的),设置LCD的窗口范围之后,连续给LCD写数据,LCD的坐标会自动自增,提高刷屏效率而已。如果你的LCD屏并不支持坐标自增或者你不会写代码,也想移植,那完全不用设置窗口那个函数,你只需要提供一个画点函数,把for循环里的刷屏代码里行扫描改掉就行。函数里的这个for循环就是主要刷出图像的代码,如果想要移植到其他LCD屏,主要就改这里,示例代码如下:for(NES_scanline = 0; NES_scanline< 240; NES_scanline++) { run6502(113*256); NES_Mapper->HSync(NES_scanline); //扫描一行 if(nes_frame==0)scanline_draw(NES_scanline); else do_scanline_and_dont_draw(NES_scanline); } 里面调用 scanline_draw 函数是按行扫描(也就是一行一行绘制图像),scanline_draw函数里面也是一个for循环,细化到每个像素点,按照每个像素点绘制到屏幕上,代码里的LCD_RAM就是当前LCD屏的地址,因为当前LCD屏采用的是FSMC,这个LCD_RAM就是FSMC地址,向这个地址写数据,FSMC就产生8080时序将数据送给LCD显示屏,刷新显示出来。scanline_draw函数详细刷屏代码如下:extern u8 nes_xoff; //显示在x轴方向的偏移量(实际显示宽度=256-2*nes_xoff) void scanline_draw(int LineNo) { uint16 i; u16 sx,ex; do_scanline_and_draw(ppu->dummy_buffer); sx=nes_xoff+8; ex=256+8-nes_xoff; if(lcddev.width==480) { for(i=sx;idummy_buffer[i]];//得到颜色值 LCD_RAM=NES_Palette[ppu->dummy_buffer[i]];//得到颜色值 i++; LCD_RAM=NES_Palette[ppu->dummy_buffer[i]];//得到颜色值 LCD_RAM=NES_Palette[ppu->dummy_buffer[i]];//得到颜色值 i++; LCD_RAM=NES_Palette[ppu->dummy_buffer[i]];//得到颜色值 LCD_RAM=NES_Palette[ppu->dummy_buffer[i]];//得到颜色值 i++; LCD_RAM=NES_Palette[ppu->dummy_buffer[i]];//得到颜色值 LCD_RAM=NES_Palette[ppu->dummy_buffer[i]];//得到颜色值 i++; LCD_RAM=NES_Palette[ppu->dummy_buffer[i]];//得到颜色值 LCD_RAM=NES_Palette[ppu->dummy_buffer[i]];//得到颜色值 i++; LCD_RAM=NES_Palette[ppu->dummy_buffer[i]];//得到颜色值 LCD_RAM=NES_Palette[ppu->dummy_buffer[i]];//得到颜色值 i++; LCD_RAM=NES_Palette[ppu->dummy_buffer[i]];//得到颜色值 LCD_RAM=NES_Palette[ppu->dummy_buffer[i]];//得到颜色值 i++; LCD_RAM=NES_Palette[ppu->dummy_buffer[i]];//得到颜色值 LCD_RAM=NES_Palette[ppu->dummy_buffer[i]];//得到颜色值 i++; LCD_RAM=NES_Palette[ppu->dummy_buffer[i]];//得到颜色值 LCD_RAM=NES_Palette[ppu->dummy_buffer[i]];//得到颜色值 i++; LCD_RAM=NES_Palette[ppu->dummy_buffer[i]];//得到颜色值 LCD_RAM=NES_Palette[ppu->dummy_buffer[i]];//得到颜色值 i++; LCD_RAM=NES_Palette[ppu->dummy_buffer[i]];//得到颜色值 LCD_RAM=NES_Palette[ppu->dummy_buffer[i]];//得到颜色值 i++; LCD_RAM=NES_Palette[ppu->dummy_buffer[i]];//得到颜色值 LCD_RAM=NES_Palette[ppu->dummy_buffer[i]];//得到颜色值 i++; LCD_RAM=NES_Palette[ppu->dummy_buffer[i]];//得到颜色值 LCD_RAM=NES_Palette[ppu->dummy_buffer[i]];//得到颜色值 i++; LCD_RAM=NES_Palette[ppu->dummy_buffer[i]];//得到颜色值 LCD_RAM=NES_Palette[ppu->dummy_buffer[i]];//得到颜色值 i++; LCD_RAM=NES_Palette[ppu->dummy_buffer[i]];//得到颜色值 LCD_RAM=NES_Palette[ppu->dummy_buffer[i]];//得到颜色值 i++; LCD_RAM=NES_Palette[ppu->dummy_buffer[i]];//得到颜色值 LCD_RAM=NES_Palette[ppu->dummy_buffer[i]];//得到颜色值 } for(i=sx;idummy_buffer[i]];//得到颜色值 LCD_RAM=NES_Palette[ppu->dummy_buffer[i]];//得到颜色值 i++; LCD_RAM=NES_Palette[ppu->dummy_buffer[i]];//得到颜色值 LCD_RAM=NES_Palette[ppu->dummy_buffer[i]];//得到颜色值 i++; LCD_RAM=NES_Palette[ppu->dummy_buffer[i]];//得到颜色值 LCD_RAM=NES_Palette[ppu->dummy_buffer[i]];//得到颜色值 i++; LCD_RAM=NES_Palette[ppu->dummy_buffer[i]];//得到颜色值 LCD_RAM=NES_Palette[ppu->dummy_buffer[i]];//得到颜色值 i++; LCD_RAM=NES_Palette[ppu->dummy_buffer[i]];//得到颜色值 LCD_RAM=NES_Palette[ppu->dummy_buffer[i]];//得到颜色值 i++; LCD_RAM=NES_Palette[ppu->dummy_buffer[i]];//得到颜色值 LCD_RAM=NES_Palette[ppu->dummy_buffer[i]];//得到颜色值 i++; LCD_RAM=NES_Palette[ppu->dummy_buffer[i]];//得到颜色值 LCD_RAM=NES_Palette[ppu->dummy_buffer[i]];//得到颜色值 i++; LCD_RAM=NES_Palette[ppu->dummy_buffer[i]];//得到颜色值 LCD_RAM=NES_Palette[ppu->dummy_buffer[i]];//得到颜色值 i++; LCD_RAM=NES_Palette[ppu->dummy_buffer[i]];//得到颜色值 LCD_RAM=NES_Palette[ppu->dummy_buffer[i]];//得到颜色值 i++; LCD_RAM=NES_Palette[ppu->dummy_buffer[i]];//得到颜色值 LCD_RAM=NES_Palette[ppu->dummy_buffer[i]];//得到颜色值 i++; LCD_RAM=NES_Palette[ppu->dummy_buffer[i]];//得到颜色值 LCD_RAM=NES_Palette[ppu->dummy_buffer[i]];//得到颜色值 i++; LCD_RAM=NES_Palette[ppu->dummy_buffer[i]];//得到颜色值 LCD_RAM=NES_Palette[ppu->dummy_buffer[i]];//得到颜色值 i++; LCD_RAM=NES_Palette[ppu->dummy_buffer[i]];//得到颜色值 LCD_RAM=NES_Palette[ppu->dummy_buffer[i]];//得到颜色值 i++; LCD_RAM=NES_Palette[ppu->dummy_buffer[i]];//得到颜色值 LCD_RAM=NES_Palette[ppu->dummy_buffer[i]];//得到颜色值 i++; LCD_RAM=NES_Palette[ppu->dummy_buffer[i]];//得到颜色值 LCD_RAM=NES_Palette[ppu->dummy_buffer[i]];//得到颜色值 i++; LCD_RAM=NES_Palette[ppu->dummy_buffer[i]];//得到颜色值 LCD_RAM=NES_Palette[ppu->dummy_buffer[i]];//得到颜色值 } }else { for(i=sx;idummy_buffer[i++]]; LCD_RAM=NES_Palette[ppu->dummy_buffer[i++]]; LCD_RAM=NES_Palette[ppu->dummy_buffer[i++]]; LCD_RAM=NES_Palette[ppu->dummy_buffer[i++]]; LCD_RAM=NES_Palette[ppu->dummy_buffer[i++]]; LCD_RAM=NES_Palette[ppu->dummy_buffer[i++]]; LCD_RAM=NES_Palette[ppu->dummy_buffer[i++]]; LCD_RAM=NES_Palette[ppu->dummy_buffer[i++]]; LCD_RAM=NES_Palette[ppu->dummy_buffer[i++]]; LCD_RAM=NES_Palette[ppu->dummy_buffer[i++]]; LCD_RAM=NES_Palette[ppu->dummy_buffer[i++]]; LCD_RAM=NES_Palette[ppu->dummy_buffer[i++]]; LCD_RAM=NES_Palette[ppu->dummy_buffer[i++]]; LCD_RAM=NES_Palette[ppu->dummy_buffer[i++]]; LCD_RAM=NES_Palette[ppu->dummy_buffer[i++]]; LCD_RAM=NES_Palette[ppu->dummy_buffer[i]]; } } }运行完刷屏的for循环函数,一帧游戏图像就显示在LCD上了。接下来就是扫描按键值,完成游戏人物的控制,函数里调用了**NesGetGamepadval()**函数,读取按键值刷新按键状态。NesGetGamepadval()函数代码如下:/* 键值说明: 开始键:8 选择建:4 方向右:128 方向左:64 方向上:16 方向下:32 功能键上/左:2 功能键下/右:1 组合键:方向右与 读取游戏手柄数据和功能键左 :130 */ void NesGetGamepadval(void) { u8 key; // PADdata0=GetJoypadKey(); //读取手柄1的值 //printf("Ó494e7f1-4e15-445b-a118-824064c956b7r\n",PADdata0); key=GetKeyValue(0); if(key==1)PADdata0=8; else if(key==2)PADdata0=128; else if(key==3)PADdata0=64; else if(key==4)PADdata0=1; else PADdata0=0; }NES游戏模拟器定义了两个全局变量,分别记录游戏手柄1和游戏手柄2的数据,因为NES游戏是可以两个人一起玩的。u8 PADdata0; //手柄1键值 [7:0]右7 左6 下5 上4 Start3 Select2 B1 A0 u8 PADdata1; //手柄2键值 [7:0]右7 左6 下5 上4 Start3 Select2 B1 A0 只需要在这个函数给这两个全局变量赋予正确的值,游戏人物就可以按照正常的动作画面出现。至于你的物理按键采用FC游戏手柄,还是普通的其他按键,只要这两个全局变量的值正确那就没问题。 所有手柄采用什么不重要,关键把代码这里逻辑看懂,看懂了你就知道程序的运行逻辑了。到此,版本1的 主要代码就分析完毕了,其他的详细过程可以看工程源码,把程序跑起来了,一切都懂了。六、工程源码分析: 以完整版本(3)为例这个版本加入了游戏手柄,VS1053、SD、FATFS文件系统等功能,这里接着第五章分析,下面就主要分析新加入的代码内容。6.1 FC游戏手柄介绍FC游戏手柄,大致可分为两种:一种手柄插口是 11 针的,一种是 9 针的。但 11 针的现在市面上很少了,现在几乎都是使用 9 针 FC 组装手柄,下面就是介绍的是 9 针 FC 手柄,该手柄还有一个特点,就是可以直接和DR9 的串口头对插!这样同开发板的连接就简单了。FC 手柄的外观如图所示:这种手柄一般有 10 个按键(实际是 8 个键值):上、下、左、右、 Start、 Select、 A、 B、 A连发、 B 连发。这里的 A 和 A 连发是一个键值,而 B 和 B 连发也是一个键值,只是连发按键当你一直按下的时候,会不停的发送(方便快速按键,比如发炮弹之类的功能)。FC 手柄的控制电路,由 1 个 8 位并入串出的移位寄存器(CD4021),外加一个时基集成电路(NE555,用于连发)构成。不过现在的手柄,为了节约成本,直接就在 PCB 上做绑定了,所以你拆开手柄,一般是看不到里面有四四方方的 IC,而只有一个黑色的小点,所有电路都集成到这个里面了,但是他们的控制和读取方法还是一样的。游戏上手柄数据读取时序从上图可看出,读取手柄按键值的信息十分简单:先 Latch(锁存键值),然后就得到了第一个按键值(A),之后在 Clock 的作用下,依次读取其他按键的键值,总共 8 个按键键值。常规状态下,LATCH为低电平,CLK为高电平,DATA为高电平,这也是初始化端口时的状态。单片机读取键值时序很简单,LATCH先发送一个高脉冲,数据将锁存到手柄内部的移位寄存器,然后在CLK时钟下降沿数据将从DATA****低位在先连续发出。按键映射到数据的对应位上,有键按下则对应位为****0,无键按下则为****1.即不按任何键时,读取数据为0xFF。键值: [7]: 右 [6]: 左 [5]: 下 [4]: 上 [3]: Start [2]: Select [1]: B [0]: A驱动代码示例:功 能:手柄初始化函数 硬件连接: CLK :PD3 --时钟线 PB10:DATA --数据线 PB11:LAT --锁存接口 */ void JoypadInit(void) { /*1. 开时钟*/ RCC->APB2ENR|=1<<5; //PD RCC->APB2ENR|=1<<3; //PB /*2. 配置模式*/ GPIOD->CRL&=0xFFFF0FFF; GPIOD->CRL|=0x00003000; GPIOB->CRH&=0xFFFF00FF; GPIOB->CRH|=0x00003800; /*3. 上拉*/ GPIOD->ODR|=1<<3; } /* 功 能:获取手柄的按键值 返回值:保存了一帧按键的状态 键值: [7]:右 [6]:左 [5]:下 [4]:上 [3]:Start [2]:Select [1]:B [0]:A */ u8 GetJoypadKey(void) { u8 key=0,i; JOYPAD_LAT=1; //开始锁存 DelayUs(30); JOYPAD_LAT=0; //锁存当前的按键状态 for(i=0;i<8;i++) { key=key>>1; if(JOYPAD_DATA==0)key|=0x80; JOYPAD_CLK=1; //输出一个上升沿,告诉手柄发送数据 DelayUs(30); JOYPAD_CLK=0; //数据线保持稳定 DelayUs(30); } return key; } 6.2 加载NES游戏:nes_load函数这里的nes_load函数和第五章的区别就是,游戏数据的来源是从SD卡读取的。传入游戏名称去SD卡上打开指定文件,读取数据进来。这里用到了外部SRAM内存,因为读出的数据需要存放到数组里,STM32F103ZET6本身的内存只有64K,肯定不够用,这里申请的空间是从外部SRAM模块里申请的,所以开发板还得带一个SRAM芯片才行,没有自带就去淘宝买一个SRAM模块即可(淘宝有个叫微雪的店铺就有卖)。详细代码如下:u8 nes_load(u8* pname) { FIL *file; UINT br; u8 res=0; file=malloc(sizeof(FIL)); if(file==0)return 1; //内存申请失败. res=f_open(file,(char*)pname,FA_READ); if(res!=FR_OK) //打开文件失败 { printf("%s 文件打开失败!\r\n",pname); free(file); return 2; } else { printf("%s 文件打开成功!\r\n",pname); } res=nes_sram_malloc(file->fsize); //申请内存 if(res==0) { f_read(file,romfile,file->fsize,&br); //读取nes文件 NESrom_crc32=get_crc32(romfile+16, file->fsize-16);//获取CRC32的值 res=nes_load_rom(); //加载ROM if(res==0) { NesClockSet(16); //UsartInit(USART1,128,115200); JoypadInit(); cpu6502_init(); //初始化6502,并复位 Mapper_Init(); //map初始化 PPU_reset(); //ppu复位 apu_init(); //apu初始化 nes_sound_open(0,APU_SAMPLE_RATE); //初始化播放设备 nes_emulate_frame(); //进入NES模拟器主循环 nes_sound_close(); //关闭声音输出 } } f_close(file); free(file);//释放内存 nes_sram_free(); //释放内存 return res; } 这里面调用了nes_sound_open函数初始化了音频设备(VS1053)。这个非常重要,要理解游戏声音是如何输出的,就认真看这里的流程。nes_sound_open函数里初始化了VS1053音频设备,然后开启了定时器中断,使用定时器去调用VS1053的播放接口,在定时器中断服务器函数里完成声音数据的输出,这里声音是存放在一个全局缓冲区里,后面游戏在主循环里运行的时候会不断的向这个缓冲区填数据,定时器超时进中断就查询是否有音乐可以播放,有就播放,没有就出来。VS1052声音播放代码示例://音频播放回调函数 void nes_vs10xx_feeddata(void) { u8 n; u8 nbytes; u8 *p; if(nesplaybuf==nessavebuf)return;//还没有收到新的音频数据 if(VS1053_DREQ!=0)//可以发送数据给VS10XX { p=nesapusbuf[nesplaybuf]+nesbufpos; nesbufpos+=32; if(nesbufpos>APU_PCMBUF_SIZE) { nesplaybuf++; if(nesplaybuf>(NES_APU_BUF_NUM-1))nesplaybuf=0; nbytes=APU_PCMBUF_SIZE+32-nesbufpos; nesbufpos=0; }else nbytes=32; for(n=0;nnes_sound_open****函数代码如下://NES打开音频输出 int nes_sound_open(int samples_per_sync,int sample_rate) { u8 *p; u8 i; p=malloc(100); //申请100字节内存 if(p==NULL)return 1; //内存申请失败,直接退出 printf("sound open:Ó494e7f1-4e15-445b-a118-824064c956b7r\n",sample_rate); for(i=0;i>8)&0XFF; p[28]=sample_rate&0XFF; //设置字节速率(8位模式,等于采样率) p[29]=(sample_rate>>8)&0XFF; nesplaybuf=0; nessavebuf=0; VS1053_Reset(); //硬复位 VS1053_SoftReset(); //软复位 VS1053_SetVol(200); //设置音量等参数 //复位解码时间 VS1053_WriteCmd(SPI_DECODE_TIME,0x0000); VS1053_WriteCmd(SPI_DECODE_TIME,0x0000); //操作两次 while(VS1053_SendMusicData(p)); //发送wav head while(VS1053_SendMusicData(p+32)); //发送wav head TimerInit(TIM6,72,1000); //1ms中断一次 free(p); //释放内存 return 1; }初始化完毕之后,就调用nes_emulate_frame函数进入到游戏主循环。6.3 游戏主循环代码现在这份代码比第五章代码增加了一个声音输出函数,调用VS1053,播放游戏的声音。apu_soundoutput函数代码如下://apu声音输出 void apu_soundoutput(void) { u16 i; apu_process(wave_buffers,APU_PCMBUF_SIZE); for(i=0;i<30;i++)if(wave_buffers[i]!=wave_buffers[i+1])break;//判断前30个数据,是不是都相等? if(i==30&&wave_buffers[i])//都相等,且不等于0 { for(i=0;i最后调用了nes_apu_fill_buffer 函数将数据赋值给VS1053缓冲区进行播放。在前面已经分析了音频初始化代码,里面初始化了定时器,会不断的查询缓冲区是否有音乐数据需要播放,有就播放,没有就输出,这个函数就是向音频缓冲区填充数据的。nes_apu_fill_buffer 函数代码如下://NES音频输出到VS1053缓存 void nes_apu_fill_buffer(int samples,u8* wavebuf) { u16 i; u8 tbuf; for(i=0;i(NES_APU_BUF_NUM-1))tbuf=0; while(tbuf==nesplaybuf)//输出数据赶上音频播放的位置了,等待. { DelayMs(5); } nessavebuf=tbuf; } 到此,音频的主要代码就分析完毕了。 可以下载程序去体验一下游戏,怀恋童年时光了
  • [技术干货] 基于STM32设计的遥控小车(手机APP+GPS+温湿度+ESP8266)
    一、环境介绍小车主控MCU: STM32F103ZET6STM32程序开发IDE: keil5STM32程序风格: 采用寄存器方式开发,注释齐全,执行效率高,方便移植手机APP: 采用QT设计,程序支持跨平台编译运行(Android、IOS、Windows、Linux都可以编译运行,对应平台上QT的环境搭建,之前博客已经发了文章讲解)硬件包含: 淘宝购买的完整一套4轮遥控小车(采用STM32F103ZET6作为主控板)、DHT11温湿度传感器、中科微GPS模块、ESP8266二、功能介绍这是基于STM32设计的4轮遥控小车,支持通过Android手机APP、Windows上位机完成对小车遥控;支持前进、后退、左转、右转、停止等操作。小车上会实时采集温度、湿度、GPS经纬度、通过ESP8266 WIFI上传至手机APP,手机APP收到数据之后,会将温湿度实时显示出来,经纬度收到后会调用百度地图,显示小车的位置,并且数据也会存放到数据库里,方便查看历史数据;支持范围内温湿度查询、最高温湿度、最低温湿度查询。小车电机驱动模块采用L298N、WIFI模块采用ESP8266、MCU采用STM32F103C8T6、温湿度模块采用DTH11、GPS模块采用北斗GPS+BDS。三、相关硬件介绍四、程序源码硬件连接说明:GPS接的串口1: PA3(RX) --5V~3.3V WIFI接的串口3: PB10(TX)--->接ESP8266的RX PB11(RX)--->接ESP8266的TX --3.3V DHT11温湿度接: PA74.1 STM32小车端: main.c源码#include "stm32f10x.h" #include "led.h" #include "delay.h" #include "key.h" #include "usart.h" #include #include "timer.h" #include "bluetooth.h" #include "esp8266.h" #include "dht11.h" #include "gps.h" #include "motor.h" /* 硬件连接说明: GPS接的串口1: PA3(RX) --5V~3.3V WIFI接的串口3: PB10(TX)--->接ESP8266的RX PB11(RX)--->接ESP8266的TX --3.3V DHT11温湿度接: PA7 */ u8 ESP8266_SendBuff[50]; char Buffer[1024]; int main() { u32 time_cnt=0; double Longitude=120.086199; double latitude=30.139219; u8 temp=20; u8 humi=70; //延时2秒保证系统稳定 delay_ms(1000); delay_ms(1000); LED_Init(); BEEP_Init(); USART1_Init(115200); //串口调试 USART2_Init(9600); //接GPS模块 TIMER2_Init(72,20000); USART3_Init(115200); //串口-WIFI ESP8166_01默认波特率9600 ESP8266_12F默认波特率115200 TIMER3_Init(72,20000); //超时时间20ms printf("正在初始化请稍等.\r\n"); printf("DHT11_Init:Õ9d9a8d3-a3c8-4ca4-858b-23e15ebfc3ecr\n",DHT11_Init());//温湿度传感器初始化 printf("准备检测WIFI硬件,请稍等...\r\n"); //初始化WIFI硬件 if(ESP8266_Init())printf("WIFI硬件错误.\r\n"); else { printf("WIFI设备正常....\r\n"); //配置WIFI的模式 192.168.4.1 printf("WIFI配置状态:Õ9d9a8d3-a3c8-4ca4-858b-23e15ebfc3ecr\n",ESP8266_AP_TCP_Server_Mode("esp8266_666","12345678",8089)); } MotorInit(); //电机初始化 //电机脉冲控制 TIMER4_Init(72,1000); while(1) { //接收到GPS的数据 if(USART2_RX_FLAG) { USART2_RX_BUFFER[USART2_RX_CNT]='\0'; //解析经纬度 GPS_GNRMC_Decoding((char*)USART2_RX_BUFFER,&Longitude,&latitude); USART2_RX_CNT=0; USART2_RX_FLAG=0; //打印到串口调试助手 printf("GPS:%f,õ9d9a8d3-a3c8-4ca4-858b-23e15ebfc3ecr\n",Longitude,latitude); } //接收到WIFI的数据 if(USART3_RX_FLAG) { USART3_RX_BUFFER[USART3_RX_CNT]='\0'; printf("WIFI:%s\r\n",USART3_RX_BUFFER); strcpy(Buffer,(char*)USART3_RX_BUFFER); USART3_RX_CNT=0; USART3_RX_FLAG=0; BEEP=1; delay_ms(50); BEEP=0; if(strstr((char*)Buffer,":a")) { printf("向前...\r\n"); CarGo(); } else if(strstr((char*)Buffer,":b")) { printf("后退...\r\n"); CarBack(); } else if(strstr((char*)Buffer,":c")) { printf("向左...\r\n"); CarLeft(); } else if(strstr((char*)Buffer,":d")) { printf("向右...\r\n"); CarRight(); } else if(strstr((char*)Buffer,":e")) { printf("停止...\r\n"); CarStop(); } } time_cnt++; delay_ms(10); //判断轮询时间 if(time_cnt>=100*2) { time_cnt=0; //读取温湿度数据 DHT11_Read_Data(&temp,&humi); sprintf((char*)ESP8266_SendBuff,"#%d,%d,%f,%f",temp,humi,Longitude,latitude); //向服务器上传数据 ESP8266_ServerSendData(0,ESP8266_SendBuff,strlen((char*)ESP8266_SendBuff)); //打印到串口调试助手 printf("ESP8266_SendBuff:%s\r\n",(char *)ESP8266_SendBuff); //运行状态 Motor_LED=!Motor_LED; } } } 4.2 STM32小车端: 电机控制源码#include "motor.h" //全局变量定义 unsigned int speed_count=0;//占空比计数器 50次一周期 int front_left_speed_duty=SPEED_DUTY; int front_right_speed_duty=SPEED_DUTY; int behind_left_speed_duty=SPEED_DUTY; int behind_right_speed_duty=SPEED_DUTY; unsigned char continue_time=0; //根据占空比驱动电机转动 void CarMove(void) { BEHIND_RIGHT_EN; //右前轮 if(front_right_speed_duty > 0)//向前 { if(speed_count < front_right_speed_duty) { FRONT_RIGHT_GO; }else //停止 { FRONT_RIGHT_STOP; } } else if(front_right_speed_duty < 0)//向后 { if(speed_count < (-1)*front_right_speed_duty) { FRONT_RIGHT_BACK; }else //停止 { FRONT_RIGHT_STOP; } } else //停止 { FRONT_RIGHT_STOP; } //左后轮 if(behind_left_speed_duty > 0)//向前 { if(speed_count < behind_left_speed_duty) { BEHIND_LEFT_GO; } else //停止 { BEHIND_LEFT_STOP; } } else if(behind_left_speed_duty < 0)//向后 { if(speed_count < (-1)*behind_left_speed_duty) { BEHIND_LEFT_BACK; } else //停止 { BEHIND_LEFT_STOP; } } else //停止 { BEHIND_LEFT_STOP; } } //向前 void CarGo(void) { front_left_speed_duty=SPEED_DUTY; front_right_speed_duty=SPEED_DUTY; behind_left_speed_duty=SPEED_DUTY; behind_right_speed_duty=SPEED_DUTY; } //后退 void CarBack(void) { front_left_speed_duty=-SPEED_DUTY; front_right_speed_duty=-SPEED_DUTY; behind_left_speed_duty=-SPEED_DUTY; behind_right_speed_duty=-SPEED_DUTY; } //向左 void CarLeft(void) { front_left_speed_duty=-20; front_right_speed_duty=SPEED_DUTY; behind_left_speed_duty=-20; behind_right_speed_duty=SPEED_DUTY+10;//增加后轮驱动力 } //向右 void CarRight(void) { front_left_speed_duty=SPEED_DUTY; front_right_speed_duty=-20; behind_left_speed_duty=SPEED_DUTY+10;//增加后轮驱动力 behind_right_speed_duty=-20; } //停止 void CarStop(void) { front_left_speed_duty=0; front_right_speed_duty=0; behind_left_speed_duty=0; behind_right_speed_duty=0; } /* FRONT_LEFT_F_PIN PG13 左前前进IO FRONT_LEFT_B_PIN PG11 左前后退IO FRONT_RIGHT_F_PIN PC11 右前前进IO FRONT_RIGHT_B_PIN PD0 右前后退IO BEHIND_LEFT_F_PIN PD6 左后前进IO BEHIND_LEFT_B_PIN PG9 左后后退IO 右后电机的两个控制IO这里改为两路使能EN1、EN2,高电平有效 BEHIND_RIGHT_F_PIN PD4 右电机使能IO BEHIND_RIGHT_B_PIN PD2 左电机使能IO */ void MotorInit(void) { RCC->APB2ENR|=1<<8; //PG RCC->APB2ENR|=1<<5; //PD RCC->APB2ENR|=1<<4; //PC GPIOG->CRH&=0xFF0F0F0F; GPIOG->CRH|=0x00303030; GPIOD->CRL&=0xF0F0F0F0; GPIOD->CRL|=0x03030303; GPIOC->CRH&=0xFFFF0FFF; GPIOC->CRH|=0x00003000; CarStop(); }4.3 STM32小车端: ESP8266 WIFI源码#include "esp8266.h" u8 ESP8266_IP_ADDR[16]; //255.255.255.255 u8 ESP8266_MAC_ADDR[18]; //硬件地址 /* 函数功能: ESP8266命令发送函数 函数返回值:0表示成功 1表示失败 */ u8 ESP8266_SendCmd(char *cmd) { u8 i,j; for(i=0;i<10;i++) //检测的次数--发送指令的次数 { USART3_RX_FLAG=0; USART3_RX_CNT=0; USARTx_StringSend(USART3,cmd); delay_ms(200); if(USART3_RX_FLAG) { USART3_RX_BUFFER[USART3_RX_CNT]='\0'; USART3_RX_FLAG=0; USART3_RX_CNT=0; if(strstr((char*)USART3_RX_BUFFER,"OK")) { return 0; } } } return 1; } /* 函数功能: ESP8266硬件初始化检测函数 函数返回值:0表示成功 1表示失败 */ u8 ESP8266_Init(void) { ESP8266_SendCmd("+++"); delay_ms(200); ESP8266_SendCmd("+++"); delay_ms(200); return ESP8266_SendCmd("AT\r\n"); } /* 函数功能: 一键配置WIFI为AP+TCP服务器模式 函数参数: char *ssid 创建的热点名称 char *pass 创建的热点密码 (最少8位) u16 port 创建的服务器端口号 函数返回值: 0表示成功 其他值表示对应错误值 */ u8 ESP8266_AP_TCP_Server_Mode(char *ssid,char *pass,u16 port) { char *p; u8 i; char tmp_buff[100]; /*1. 测试硬件*/ if(ESP8266_SendCmd("AT\r\n"))return 1; /*2. 关闭回显*/ if(ESP8266_SendCmd("ATE0\r\n"))return 2; /*3. 设置WIFI模式*/ if(ESP8266_SendCmd("AT+CWMODE=2\r\n"))return 3; /*4. 复位*/ ESP8266_SendCmd("AT+RST\r\n"); delay_ms(1000); delay_ms(1000); delay_ms(1000); /*5. 关闭回显*/ if(ESP8266_SendCmd("ATE0\r\n"))return 5; /*6. 设置WIFI的AP模式参数*/ sprintf(tmp_buff,"AT+CWSAP=\"%s\",\"%s\",1,4\r\n",ssid,pass); if(ESP8266_SendCmd(tmp_buff))return 6; /*7. 开启多连接*/ if(ESP8266_SendCmd("AT+CIPMUX=1\r\n"))return 7; /*8. 设置服务器端口号*/ sprintf(tmp_buff,"AT+CIPSERVER=1,Õ9d9a8d3-a3c8-4ca4-858b-23e15ebfc3ecr\n",port); if(ESP8266_SendCmd(tmp_buff))return 8; /*9. 查询本地IP地址*/ if(ESP8266_SendCmd("AT+CIFSR\r\n"))return 9; //提取IP地址 p=strstr((char*)USART3_RX_BUFFER,"APIP"); if(p) { p+=6; for(i=0;*p!='"';i++) { ESP8266_IP_ADDR[i]=*p++; } ESP8266_IP_ADDR[i]='\0'; } //提取MAC地址 p=strstr((char*)USART3_RX_BUFFER,"APMAC"); if(p) { p+=7; for(i=0;*p!='"';i++) { ESP8266_MAC_ADDR[i]=*p++; } ESP8266_MAC_ADDR[i]='\0'; } //打印总体信息 printf("当前WIFI模式:AP+TCP服务器\n"); printf("当前WIFI热点名称:%s\n",ssid); printf("当前WIFI热点密码:%s\n",pass); printf("当前TCP服务器端口号:Õ9d9a8d3-a3c8-4ca4-858b-23e15ebfc3ecn",port); printf("当前TCP服务器IP地址:%s\n",ESP8266_IP_ADDR); printf("当前TCP服务器MAC地址:%s\n",ESP8266_MAC_ADDR); return 0; } /* 函数功能: TCP服务器模式下的发送函数 发送指令: */ u8 ESP8266_ServerSendData(u8 id,u8 *data,u16 len) { u8 i,j,n; char ESP8266_SendCMD[100]; //组合发送过程中的命令 for(i=0;i<10;i++) { sprintf(ESP8266_SendCMD,"AT+CIPSEND=%d,Õ9d9a8d3-a3c8-4ca4-858b-23e15ebfc3ecr\n",id,len); USARTx_StringSend(USART3,ESP8266_SendCMD); delay_ms(200); if(USART3_RX_FLAG) { USART3_RX_BUFFER[USART3_RX_CNT]='\0'; USART3_RX_FLAG=0; USART3_RX_CNT=0; if(strstr((char*)USART3_RX_BUFFER,">")) { //继续发送数据 USARTx_DataSend(USART3,data,len); //等待数据发送成功 delay_ms(200); if(USART3_RX_FLAG) { USART3_RX_BUFFER[USART3_RX_CNT]='\0'; USART3_RX_FLAG=0; USART3_RX_CNT=0; if(strstr((char*)USART3_RX_BUFFER,"SEND OK")) { return 0; } } } } } return 1; }4.4 QT软件端代码布局
  • [技术干货] 基于STM32设计的校园一卡通项目-详解原理
    一、IC卡介绍常用的IC卡一般是M1卡,也称为S50卡,购买RC522刷卡模块送的白卡,蓝色钥匙扣、公交卡、地铁卡都是S50卡。S50卡内部有16个分区,每分区有AB两组密码,总容量为8Kbit。第0个扇区第0块用于存放厂商代码,意见固话,不可更改。每个扇区的块0、块1、块2为数据块,可以用于存储数据。数据块可以进行读写操作。每个扇区的块3为控制块,包括了密码A、存储控制、密码B。具体结构如下: 每个扇区的密码和控制位都是独立的,可以根据实际需求设定各自的密码及存取控制。存取控制为4个字节,共32位,扇区中的每个块(包括数据和控制块)存取条件是由密码和存取控制共同决定的,在存取控制中每个块都有一个相应的三个控制位。重点总结:(1)M1卡分为16个扇区,每个扇区由4块(0、1、2、3)组成。在实际操作时,将16个扇区分为64个块,按绝对地址编号为0-63进行访问,也就是程序里需要填块的位置时,范围是0~63。(2)每个块的大小是16字节,每个扇区里有3个数据块,数据块可以存放自己的自定义数据。二、一卡通消费机实现原理2.1 封装核心函数(1)主要的硬件:单片机选择STM32,刷卡模块采用RC522。(2)实现核心思路:为了方便存储数据,对数据进行管理,保证程序的通用性,将IC卡的所有信息都存放在IC卡上。包括:激活状态、卡所属人信息,金额等。所以在程序里定义了一个结构体:#pragma pack(1) //这个结构体大小为16个字节,刚好存放到 IC卡的一个块里面 typedef struct CARD_INFO { u8 stat; //卡状态. 66表示此卡已经激活 其他值表示此卡未激活 // 88表示此卡挂失,无法再进行消费 u32 money; //金额. 第一次激活卡,就将金额清0 u8 phone[11];//可以存放电话号码,ID,标识符之类的数据 }CARD; extern u8 IC_Card_uid[4];并封装了两个底层函数: 接下来的所有对卡的操作只需要调用下面函数即可。//读取卡号 u8 IC_Card_uid[4]; /* card_uid :卡的id号外部5字节数组 data : 读出来的一个块,16字节数据 addr : 块号,从4开始 数据存放的地址。每个扇区的0、1、2块是存放数据。3是存放密码。 一般填:0、1、2 、4、5、6、8、9、10 数据一般格式:u8 SJ[16]={255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255}; //写入的金额; */ u8 IC_Card_Read(CARD *rdata) { u8 KEY[6] = {0xff,0xff,0xff,0xff,0xff,0xff}; //白卡的出厂密码 u8 status; /*1. 寻卡*/ status = search_card(IC_Card_uid); /*2. 验证卡密码*/ if(MI_OK==status) { print_CardNnmber(IC_Card_uid); status = RC522_PcdAuthState(PICC_AUTHENT1A, 3, KEY, IC_Card_uid); //验证卡片密码 形参参数:验证方式,块地址,密码,卡序列号 } /*3. 读出数据*/ if(MI_OK==status) { status = RC522_PcdRead(1,(u8*)rdata); //从第addr块读出数据值。 } return status; } /* 功能:写数据到指定块 参数: u8 addr :数据存放的地址。每个扇区的0、1、2块是存放数据。3是存放密码。 一般填:0、1、2 、4、5、6、8、9、10 数据一般格式:u8 SJ[16]={255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255}; //写入的金额; */ u8 IC_Card_Write(CARD *wdata) { u8 KEY[6] = {0xff,0xff,0xff,0xff,0xff,0xff}; //白卡的出厂密码 u8 status; /*1. 寻卡*/ status = search_card(IC_Card_uid); /*2. 验证卡密码*/ if(MI_OK==status) { status = RC522_PcdAuthState(PICC_AUTHENT1A, 3, KEY, IC_Card_uid); //验证卡片密码 形参参数:验证方式,块地址,密码,卡序列号 } /*3. 写数据到卡*/ if(MI_OK==status) { status = RC522_PcdWrite(1, (u8*)wdata); //写数据到第addr块,data入的数据值。 } return status; }2.2 编写案例接口为了方便理解整体的设计思路,下面针对几个常见的操作编写了函数接口测试Demo。void Activation_CardInformation(void); //对卡激活-将卡状态设置为66 void Unlock_CardInformation(void); //对卡解锁--去除挂失状态。将卡状态设置为66 void locking_CardInformation(void); //对卡挂失。将卡状态设置为88 void Consumption_CardInformation(void); //消费. 消费就是减少金额. void Recharge_CardInformation(void); //对卡进行充值. 充值就是累加金额 void Query_CardInformation(void); //查询卡的详细信息,通过串口打印源代码如下:#include "app.h" /* 函数功能: 查询卡的详细信息,通过串口打印 */ void Query_CardInformation() { CARD data; if(IC_Card_Read(&data)==MI_OK) { //判断卡是否已经激活 if(data.stat==66) { printf("用户信息:%s\r\n",data.phone); printf("余额:Õc2d497a-4828-4cbb-b6df-dd58422d45b7r\n",data.money); } else if(data.stat==88) { printf("此卡已挂失.请先解锁.\r\n"); } //卡没有激活 else { printf("此卡没有激活.\r\n"); } //复位--释放选中的卡片 RC522_PcdReset(); } } /* 函数功能: 对卡进行充值. 充值就是累加金额 */ void Recharge_CardInformation() { CARD data; if(IC_Card_Read(&data)==MI_OK) { //判断卡是否已经激活 if(data.stat==66) { printf("用户信息:%s\r\n",data.phone); printf("充值前的余额:Õc2d497a-4828-4cbb-b6df-dd58422d45b7r\n",data.money); //累加金额 data.money+=100; //充值100块 //重新写入到卡里 RC522_PcdWrite(1, (u8*)&data); //写数据到第addr块,data入的数据值。; printf("充值后的余额:Õc2d497a-4828-4cbb-b6df-dd58422d45b7r\n",data.money); } //卡已经挂失 else if(data.stat==88) { printf("此卡已挂失.请先解锁后再充值.\r\n"); } //卡没有激活 else { printf("此卡没有激活.请先激活后再充值.\r\n"); } //复位--释放选中的卡片 RC522_PcdReset(); } } /* 函数功能: 消费. 消费就是减少金额. */ void Consumption_CardInformation() { CARD data; if(IC_Card_Read(&data)==MI_OK) { //判断卡是否已经激活 if(data.stat==66) { printf("用户信息:%s\r\n",data.phone); printf("消费前的余额:Õc2d497a-4828-4cbb-b6df-dd58422d45b7r\n",data.money); //消费金额,假如:我要消费10元,先判断卡里有没有10元,没有就不能消费. printf("即将消费10元...\r\n"); //余额足够才能消费 if(data.money>=10) { data.money-=10; //减去10块 //重新写入到卡里 RC522_PcdWrite(1, (u8*)&data); //写数据到第addr块,data入的数据值。; printf("消费后的余额:Õc2d497a-4828-4cbb-b6df-dd58422d45b7r\n",data.money); } else { printf("余额不足,消费失败...\r\n"); } } //卡已经挂失 else if(data.stat==88) { printf("此卡已挂失.请先解锁后再进行消费流程.\r\n"); } //卡没有激活 else { printf("此卡没有激活.请先激活后再进行消费流程.\r\n"); } //复位--释放选中的卡片 RC522_PcdReset(); } } /* 函数功能: 对卡挂失。将卡状态设置为88 */ void locking_CardInformation() { CARD data; if(IC_Card_Read(&data)==MI_OK) { //判断卡是否已经激活 if(data.stat==66) { printf("用户信息:%s\r\n",data.phone); //设置挂失状态 data.stat=88; //重新写入到卡里 RC522_PcdWrite(1, (u8*)&data); //写数据到第addr块,data入的数据值。; printf("此卡已成功挂失.\r\n"); } //卡已经挂失 else if(data.stat==88) { printf("此卡已挂失.\r\n"); } //卡没有激活 else { printf("此卡没有激活.请先激活.\r\n"); } //复位--释放选中的卡片 RC522_PcdReset(); } } /* 函数功能: 对卡解锁--去除挂失状态。将卡状态设置为66 */ void Unlock_CardInformation() { CARD data; if(IC_Card_Read(&data)==MI_OK) { //判断卡是否已经激活 if(data.stat==66) { printf("此卡已解锁.\r\n"); } //卡已经挂失 else if(data.stat==88) { //设置解锁状态 data.stat=66; //重新写入到卡里 RC522_PcdWrite(1, (u8*)&data); //写数据到第addr块,data入的数据值。; printf("此卡已成功解锁.\r\n"); } //卡没有激活 else { printf("此卡没有激活.请先激活.\r\n"); } //复位--释放选中的卡片 RC522_PcdReset(); } } /* 函数功能: 对卡激活-将卡状态设置为66 激活卡也叫注册卡。可以写入一些用户信息到卡里. */ void Activation_CardInformation() { CARD data; if(IC_Card_Read(&data)==MI_OK) { //判断卡是否已经激活 if(data.stat==66) { printf("此卡已激活,不需要重复激活.\r\n"); } //卡已经挂失 else if(data.stat==88) { printf("此卡已激活,并挂失,锁定,请先解锁...\r\n"); } //卡没有激活 else { //设置解锁状态 data.stat=66; strncpy((char*)data.phone,"473608901",sizeof(data.phone)-1); //重新写入到卡里 // IC_Card_Write(&data); /*3. 写数据到卡*/ RC522_PcdWrite(1, (u8*)&data); //写数据到第addr块,data入的数据值。 printf("此卡已成功激活.用户信息:%s\r\n",data.phone); } //复位--释放选中的卡片 RC522_PcdReset(); } } 2.3 编写操作界面为了方便测试功能,在LCD屏上绘制了几个矩形,触摸屏点击分别执行对应的功能。#include "app.h" /* RC522射频模块外部的接口: *1--SDA <----->PB5--片选脚 *2--SCK <----->PB4--时钟线 *3--MOSI<----->PA12--输出 *4--MISO<----->PA11--输入 *5--悬空 *6--GND <----->GND *7--RST <----->PA8--复位脚 *8--VCC <----->VCC */ int main() { USARTx_Init(USART1,72,115200); LCD_Init(); LCD_Clear(BLACK); XPT2046_TouchInit(); RC522_Init(); // DisplayString(0,0,16,"12345jkdbdfvdfvdfv7364837340hdxsmsks3743934ndvdfv",BLACK,WHITE); // // POINT_COLOR=0x00FF; //设置画笔颜色 // LCD_DrawLine(0,0,200,50); //画线 // //颜色填充 LCD_Fill(0,0,120,105,RED); //颜色填充 LCD_Fill(120,0,239,105,RED); //颜色填充 LCD_Fill(0,105,120,210,RED); //颜色填充 LCD_Fill(120,105,239,210,RED); //颜色填充 LCD_Fill(0,210,120,320,RED); //颜色填充 LCD_Fill(120,210,239,320,RED); DisplayString(0,0,16,"Activation",BLACK,WHITE); DisplayString(120,0,16,"Query",BLACK,WHITE); DisplayString(0,105,16,"Recharge",BLACK,WHITE); DisplayString(120,105,16,"Consumption",BLACK,WHITE); DisplayString(0,210,16,"locking",BLACK,WHITE); DisplayString(120,210,16,"Unlock",BLACK,WHITE); while(1) { //扫描触摸屏坐标 if(XPT2046_ReadXY()) { printf("x=%d,y=Õc2d497a-4828-4cbb-b6df-dd58422d45b7r\n",xpt2046_touch.x,xpt2046_touch.y); printf("x0=%d,y0=Õc2d497a-4828-4cbb-b6df-dd58422d45b7r\n",xpt2046_touch.x0,xpt2046_touch.y0); // 对卡激活- if(xpt2046_touch.x>0&&xpt2046_touch.x<120&& xpt2046_touch.y>0&&xpt2046_touch.y<105) { printf("---- 对卡激活-Demo----\r\n"); //充值Demo Activation_CardInformation(); //颜色填充 LCD_Fill(0,0,120,105,WHITE); DisplayString(0,0,16,"Activation",BLACK,WHITE); //等待触摸屏松开 while(XPT2046_PEN==0){} //颜色填充 LCD_Fill(0,0,120,105,RED); DisplayString(0,0,16,"Activation",BLACK,WHITE); } //查询Demo else if(xpt2046_touch.x>120&&xpt2046_touch.x<240&& xpt2046_touch.y>0&&xpt2046_touch.y<105) { printf("----运行查询Demo----\r\n"); //查询Demo Query_CardInformation(); //颜色填充 LCD_Fill(120,0,239,105,WHITE); DisplayString(120,0,16,"Query",BLACK,WHITE); //等待触摸屏松开 while(XPT2046_PEN==0){} //颜色填充 LCD_Fill(120,0,239,105,RED); DisplayString(120,0,16,"Query",BLACK,WHITE); } //充值Demo else if(xpt2046_touch.x>0&&xpt2046_touch.x<120&& xpt2046_touch.y>105&&xpt2046_touch.y<210) { printf("----运行充值Demo----\r\n"); //充值Demo Recharge_CardInformation(); //颜色填充 LCD_Fill(0,105,120,210,WHITE); DisplayString(0,105,16,"Recharge",BLACK,WHITE); //等待触摸屏松开 while(XPT2046_PEN==0){} //颜色填充 LCD_Fill(0,105,120,210,RED); DisplayString(0,105,16,"Recharge",BLACK,WHITE); } //消费Demo else if(xpt2046_touch.x>120&&xpt2046_touch.x<240&& xpt2046_touch.y>105&&xpt2046_touch.y<210) { printf("----运行消费Demo----\r\n"); //消费Demo Consumption_CardInformation(); //颜色填充 LCD_Fill(120,105,239,210,WHITE); DisplayString(120,105,16,"Consumption",BLACK,WHITE); //等待触摸屏松开 while(XPT2046_PEN==0){} //颜色填充 LCD_Fill(120,105,239,210,RED); DisplayString(120,105,16,"Consumption",BLACK,WHITE); //等待触摸屏松开 } //挂失Demo else if(xpt2046_touch.x>0&&xpt2046_touch.x<120&& xpt2046_touch.y>210&&xpt2046_touch.y<320) { printf("----运行挂失Demo----\r\n"); //挂失Demo locking_CardInformation(); //颜色填充 LCD_Fill(0,210,120,320,WHITE); DisplayString(0,210,16,"locking",BLACK,WHITE); //等待触摸屏松开 while(XPT2046_PEN==0){} //颜色填充 LCD_Fill(0,210,120,320,RED); DisplayString(0,210,16,"locking",BLACK,WHITE); } //解锁Demo else if(xpt2046_touch.x>120&&xpt2046_touch.x<240&& xpt2046_touch.y>210&&xpt2046_touch.y<320) { printf("----运行解锁Demo----\r\n"); //解锁Demo Unlock_CardInformation(); //颜色填充 LCD_Fill(120,210,239,320,WHITE); DisplayString(120,210,16,"Unlock",BLACK,WHITE); //等待触摸屏松开 while(XPT2046_PEN==0){} //颜色填充 LCD_Fill(120,210,239,320,RED); DisplayString(120,210,16,"Unlock",BLACK,WHITE); } } delay_ms(10); } }2.4 运行效果
  • [技术干货] 基于STM32设计的小说阅读器(翻页、字体切换、颜色切换、语音播报)
    一、环境介绍小车主控MCU: STM32F103ZET6STM32程序开发IDE: keil5STM32程序风格: 采用寄存器方式开发,注释齐全,执行效率高,方便移植硬件包含: 一块STM32F103ZET6系统板、一个2.8寸TFT电阻触摸显示屏、一个SD卡卡槽(SPI接口)、一张SD卡(存放字库和小说文件)二、功能介绍这是基于ST32F103ZET6设计的小说阅读器,虽然对于真实的小说阅读器产品来讲,实用性和功能方面还差很多,但是对于刚入门的STM32、单片机开发工程师来讲,这里面设计到的技术才是最有价值的。所以这篇文章的小说阅读器主要是用来作为嵌入式单片机工程师入门练手项目、大学生的课程设计等。目的不在于小说阅读器,而是以小说阅读器为例子,学习相关的技术: SD卡、串口通信、SPI通信、8080时序、触摸屏校准原理、FATFS文件系统使用、语音播报模块使用等等。该阅读器支持常规阅小说读器具备的基本功能:(1). 支持选择指定的小说进行查看阅读,可以通过触摸屏上的按钮进行切换。(2). 支持切换字体大小(3). 支持切换字体颜色、背景颜色(4). 标题栏显示当前阅读器查看的小说文件名称(5). 支持翻页、上一页、下一页(6). 支持语音自动阅读,发声接近正常真人发声,非常强大。语音方案可以选择两种: (1). 宇音SYN6658 (2). 科大讯飞SYN5152。 这两款芯片都是通过串口通信,编程十分简单。内部编程思路介绍:小说阅读器的字体是存放在SD卡上的,SD卡采用SPI接口的卡槽与STM32相连接,STM32配合FATFS文件系统对SD卡上的文件进行操作;为了提高访问效率、在第一次上电的时候会将SD卡上的字库文件拷贝到板载W25Q64芯片内。小说文件还是存放在SD卡上,每次翻页的时候从SD卡上获取文本文件,渲染到LCD显示屏上。该显示屏是2.8寸的电阻触摸显示屏,驱动芯片是ILI9341(兼容:9325,9328),LCD的引脚接线兼容正点原子的2.8寸LCD显示屏;电阻屏的驱动芯片是XPT2046,,是很常见的组合,这个XPT2046就是个ADC芯片,最终要完成触摸屏上坐标点定位,还需要自己写校准算法进行换算。 ILI9341驱动芯片支持8080时序操作,可以采用IO模拟方式驱动、也可以采用STM32的FSMC接口驱动。 STM32增强版支持FSMC功能的,其他没有FSMC接口的芯片,可以采用模拟8080时序方式驱动,效果一样,只是效率上差点,无法实现高速刷屏,只要不进行高速刷屏,凑合使用是没什么问题的。三、所用到的硬件介绍(都是淘宝买的)3.1 STM32F103ZET6最小系统板这是在淘宝上买的硬件详情,开发板和LCD用哪一款都可以的,编程思路都是一样。开发板的板载资源如下: CPU:STM32F103ZET6,LQFP144,FLASH:512K,SRAM:64K; 外扩SPI FLASH:W25Q32,8M字节; 1个电源指示灯; 2个状态指示灯; 一个EEPROM芯片,24C02,容量256字节(注意:不同产地标号不一,但都是24C02芯片,经测试无误) 1个光敏传感器; 1个无线模块接口,可接NRF24L01/RFID/CC01模块; 1路CAN接口,采用TJA1050芯片; 1路485接口,采用SP485芯片; 1个标准的2.4/2.8/3.5/4.3/7寸LCD接口,支持触摸屏; 一个USB串口,可用于程序下载和代码调试(USMART调试); 1个USB SLAVE接口,用于USB通信; 1个复位按键; 2个独立按键; 1个SD卡座,用来接SD卡; 1个RTC后备电池座; 1个标准的JTAG/SWD仿真下载调试接口; 1路5V转3.3V电路; 芯片引脚144个脚全部引出,方便外接扩展实验; 1个电源开关,用来开关USB的电源;3.2 SD卡卡槽3.3 SYN6658语音合成芯片功能特点: • 芯片支持任意中文文本的合成,可以采用GB2312、GBK、BIG5 和Unicode 四种编码方式; • 芯片具有文本智能分析处理功能,对常见的数值、电话号码、时间日期、度量衡符号等格式的文本; • 芯片可以自动对文本进行分析,判别文本中多音字的读法并合成正确的读音; • 芯片可实现10级数字音量控制,音量更大,更广; • 芯片内集成了77首声音提示音和14首和弦音乐; • 提供两男、两女、一个效果器和一个女童声共6个中文发音人; • 支持多种文本控制标记,提升文本处理的正确率; • 支持多种控制命令,包括:合成、停止、暂停合成、继续合成、改变波特率等; • 支持多种方式查询芯片的工作状态; • 两种通讯模式:芯片支持UART、SPI两种通讯方式; • 芯片支持Power Down 模式。使用控制命令可以使芯片进入Power Down 模式; • 芯片支持的通讯波特率:4800bps,9600bps,57600bps、115200bps; • 芯片各项指标均满足室外严酷环境下的应用;应用范围: • 车载信息终端语音播报,车载调度,车载导航 • 停车场收费系统/诱导系统 • 公交报站器 ,考勤机 • 手机,固定电话 • 排队叫号机,收银收费机 • 自动售货机,信息机, POS 机 • 智能仪器仪表 ,气象预警机,智能变压器 • 智能玩具,智能手表 • 电动自行车 • 语音电子书,彩屏故事书,语音电子词典,语音电子导游 • 短消息播放 ,新闻播放 • 电子地图四、操作说明4.1 程序下载开发板支持Jlink下载、也支持串口下载。4.2 屏幕操作说明目前实现的功能: (1). 小说翻页:支持点击触摸屏按钮翻下一页显示 (2). 换小说:点击触摸屏按钮“下一本”,可以切换小说。 (3). 换颜色:点击触摸屏按钮“颜色调整”,可以切换颜色,支持12种字体颜色切换。 (4). 换字体:点击触摸屏按钮“字体调整”,可以切换字体,目前支持两种字体(16X16 24X24)。思路说明: 程序里移植了FATFS文件系统,字体文件和小说文件都是存放在SD卡,通过文件系统读取SD卡里的小说文件进行显示。操作的过程在串口调试助手上也会同步输出信息。4.3 校准说明第一次使用,需要校准屏幕,否则触摸屏没有反应。如果发现屏幕不灵敏,可以强制进行校准,按下按键K2再按下复位键即可进行强制校准。依次点击屏幕上4个红圈。4.4 SD卡上存放的文件SD卡上有两个目录:font目录和txt目录。font目录:存放字库文件。有两个字库字体。txt目录:存放小说文件,内置了3篇小说。五、核心代码代码采用Keil5编写,下载即可编译,测试,学习。5.1 main.c 主函数代码#include "stm32f10x.h" #include "delay.h" #include "sys.h" #include "usart.h" #include #include #include "iic.h" #include "at24c08.h" #include "w25q64.h" #include "nt35310_lcd.h" #include "xpt2046.h" #include "sdcard.h" #include "ff.h" //FATFS文件系统的头文件 //更新字库---从SD卡读取字库到W25Q64 void FontUpdate_to_W25Q64(); FATFS fatfs; //文件系统注册工作区需要使用 u16 select_color[]={WHITE,BLACK,BLUE,RED,YELLOW,BROWN,BRRED,GRAY,DARKBLUE,LIGHTBLUE,GRAYBLUE,LIGHTGREEN}; u8 read_text_buf[4096+1]; int main() { u32 x;u32 y;u32 size=16;u8 *p; u8 color_select_cnt=0; //12个 FIL text_file; u16 br=0; u8 r_data=10; u32 read_cnt=0; DIR dir; FRESULT res; FILINFO fno; //存放读取的文件信息 char *abs_path=NULL; char path[]="0:/txt"; u32 cnt=0; USART_X_Init(USART1,72,115200); NT35310_LcdInit(); NT35310_Clear(WHITE); IIC_Init(); //IIC总线初始化 W25Q64_Init(); //初始化W25Q64 TOUCH_Init(); //触摸屏初始化 TOUCH_CheckXY(); //触摸屏校准程序 RCC->APB2ENR|=1<<5; GPIOD->CRH&=0xFF0FFFFF; GPIOD->CRH|=0x00300000; while(SDCardDeviceInit()!=0) { printf("SDCard_DeviceInit 错误.\r\n"); PDout(13)=!PDout(13); delay_ms(100); } f_mount(&fatfs,"0:",0); //注册文件系统的工作区 //设计界面 LCD_color_1=RED; LCD_color_2=LIGHTBLUE; NT35310_DisplayString(16,0,16,"基于STM32的小说阅读器设计"); NT35310_DrawLine(0,16,239,16,DARKBLUE); //绘制按键 NT35310_DrawRectangle(0,319-80,239,319,RED); NT35310_DrawLine(0,319-40,239,319-40,DARKBLUE); NT35310_DrawLine(239/2,319-80,239/2,319,DARKBLUE); LCD_color_2=WHITE; NT35310_DisplayString(32,319-70,16,"下一页"); NT35310_DisplayString(239/2+32,319-70,16,"下一本"); NT35310_DisplayString(32,319-30,16,"字体调整"); NT35310_DisplayString(239/2+32,319-30,16,"颜色调整"); /*1. 打开目录*/ res=f_opendir(&dir,path); if(res!=FR_OK)return res; res=f_readdir(&dir,&fno); printf("文件名称: %s,文件大小: %ld 字节\r\n",fno.fname,fno.fsize); LCD_color_1=BLACK; NT35310_DisplayString(0,17,16,fno.fname); if(abs_path) { free(abs_path); abs_path=NULL; } //申请存放文件名称的长度 abs_path=malloc(strlen(path)+strlen(fno.fname)+1); strcpy(abs_path,path); strcat(abs_path,"/"); strcat(abs_path,fno.fname); printf("abs_path=%s\n",abs_path); NT35310_DisplayString(0,17+16,16,"第1卷\ 第一回 甄士隐梦幻识通灵 贾雨村风尘怀闺秀\ 此开卷第一回也。作者自云:因曾历过一番梦幻之后,故将真事隐去,\ 而借“通灵”之说,撰此<<石头记>>一书也。故曰“甄士隐”云云。\ 但书中所记何事何人?自又云:“今风尘碌碌,一事无成,忽念及当日所有之女子,\ 一一细考较去,觉其行止见识,皆出于我之上。何我堂堂须眉,诚不若彼裙钗哉?"); while(1) { if(TOUCH_PEN==0) //判断触摸屏是否按下 { //判断是否读取到XY坐标 if(TOUCH_ReadXY()) { // printf("x=%d,y=Ñ18a5b24-3742-46d6-b3ef-8e61dbca6637r\n",touch_info.x,touch_info.y); //判断范围 if((touch_info.x>=0 && touch_info.x<=239/2)&& (touch_info.y>=319-80 && touch_info.y<=319-40)) { LCD_color_2=BLUE; //填充颜色 NT35310_Fill(0+1,319-80+1,239/2-1,319-40-1,BLUE); //显示字符串 NT35310_DisplayString(32,319-70,16,"下一页"); //等待触摸屏松开 while(TOUCH_PEN==0){} //填充颜色--清屏 NT35310_Fill(0,18+16,239,319-80-1,WHITE); LCD_color_2=WHITE; if(read_cnt>=br) { read_cnt=0; } if(read_cnt==0) { if(br!=4096) { res=f_open(&text_file,(const TCHAR*)abs_path,FA_READ);//打开文件 if(res!=0) { printf("%s文件打开失败!\r\n",abs_path); return 1; //文件打开失败 } printf("%s文件打开成功!\n",abs_path); } //执行代码 res=f_read(&text_file,read_text_buf,4096,(UINT*)&br);//读出4096个字节 read_text_buf[br]='\0'; printf("br=Ñ18a5b24-3742-46d6-b3ef-8e61dbca6637r\n",br); if(br!=4096) { f_close(&text_file); } } //字体大小 x=0; //坐标起始位置 y=17+16; //坐标起始位置 p=read_text_buf+read_cnt; while(*p!='\0') { if(*p>0x80) //判断是否是中文-编码规则从 0X8140 开始 { read_cnt+=2; if(x+size>239) { x=0; //横坐归0 y+=size; //换行 if(y+size>=319-80-1)break; } NT35310_DisplayGBKData(x,y,size,p);//显示一个中文 x+=size; p+=2; //偏移两个字节 } else if(*p>=' ' && *p<='~') //常用的ASCII码 { read_cnt+=1; if(x+size/2>239) { x=0; //横坐归0 y+=size; //换行 if(y+size>=319-80-1)break; } if(size==16) { //显示英文字母 NT35310_DisplayData(x,y,size/2,size,(u8*)ASCII_8_16[*p-' ']); } else if(size==24) { //显示英文字母 NT35310_DisplayData(x,y,size/2,size,(u8*)asc2_2412[*p-' ']); } p+=1; x+=size/2; } else if(*p=='\n') { x=0; y+=size; p+=1; //偏移指针 read_cnt+=1; if(y+size>=319-80-1)break; } else { read_cnt+=1; p+=1; //偏移指针 } } //填充颜色 NT35310_Fill(0+1,319-80+1,239/2-1,319-40-1,WHITE); LCD_color_2=WHITE; //显示字符串 NT35310_DisplayString(32,319-70,16,"下一页"); } //判断范围 if((touch_info.x>=239/2 && touch_info.x<=239)&& (touch_info.y>=319-80 && touch_info.y<=319-40)) { LCD_color_2=BLUE; //填充颜色 NT35310_Fill(239/2+1,319-80+1,239-1,319-40-1,BLUE); //显示字符串 NT35310_DisplayString(239/2+32,319-70,16,"下一本"); //等待触摸屏松开 while(TOUCH_PEN==0){} LCD_color_2=WHITE; //关闭原来的文件 f_close(&text_file); //触发新的页 read_cnt=0; br=0; //执行代码 res=f_readdir(&dir,&fno); if(fno.fname[0] == 0 || res!=0) { /*3. 关闭目录*/ f_closedir(&dir); /*1. 打开目录*/ res=f_opendir(&dir,path); if(res!=FR_OK)return res; res=f_readdir(&dir,&fno); } printf("文件名称: %s,文件大小: %ld 字节\r\n",fno.fname,fno.fsize); LCD_color_1=BLACK; NT35310_DisplayString(0,17,16,fno.fname); if(abs_path) { free(abs_path); abs_path=NULL; } //申请存放文件名称的长度 abs_path=malloc(strlen(path)+strlen(fno.fname)+1); strcpy(abs_path,path); strcat(abs_path,"/"); strcat(abs_path,fno.fname); printf("abs_path=%s\n",abs_path); //填充颜色 NT35310_Fill(239/2+1,319-80+1,239-1,319-40-1,WHITE); //显示字符串 NT35310_DisplayString(239/2+32,319-70,16,"下一本"); } //判断范围 if((touch_info.x>=0 && touch_info.x<=239/2)&& (touch_info.y>=319-40 && touch_info.y<=319)) { LCD_color_2=BLUE; //填充颜色 NT35310_Fill(0+1,319-40+1,239/2-1,319-1,BLUE); //显示字符串 NT35310_DisplayString(32,319-30,16,"字体调整"); //等待触摸屏松开 while(TOUCH_PEN==0){} if(size==16)size=24; else size=16; //执行代码 //填充颜色 NT35310_Fill(0+1,319-40+1,239/2-1,319-1,WHITE); LCD_color_2=WHITE; //显示字符串 NT35310_DisplayString(32,319-30,16,"字体调整"); } //判断范围 if((touch_info.x>=239/2 && touch_info.x<=239)&& (touch_info.y>=319-40 && touch_info.y<=319)) { LCD_color_2=BLUE; //填充颜色 NT35310_Fill(239/2+1,319-40+1,239-1,319-1,BLUE); //显示字符串 NT35310_DisplayString(239/2+32,319-30,16,"颜色调整"); //等待触摸屏松开 while(TOUCH_PEN==0){} //执行代码 //前景字体颜色切换 LCD_color_1=select_color[color_select_cnt++]; if(color_select_cnt>=12) { color_select_cnt=0; } //填充颜色 NT35310_Fill(239/2+1,319-40+1,239-1,319-1,WHITE); LCD_color_2=WHITE; //显示字符串 NT35310_DisplayString(239/2+32,319-30,16,"颜色调整"); } } } } } u32 gbk32_32_addr=1024*0; u8 font_buffer[4096]; //更新字库---从SD卡读取字库到W25Q64 void FontUpdate_to_W25Q64() { u32 w_cnt=0; FILINFO fno; FIL fp; UINT br,res; /*1. 打开字库*/ f_open(&fp,"0:/font/gbk16.DZK",FA_READ); /*2. 循环读取字库更新到W25Q64*/ f_stat("0:/font/gbk16.DZK",&fno); printf("文件的大小:Ñ18a5b24-3742-46d6-b3ef-8e61dbca6637r\n",fno.fsize); while(1) { /*3. 读取字库文件*/ res=f_read(&fp,font_buffer,4096,&br); /*4. 写入到W25Q64里*/ W25Q64_WriteData(gbk32_32_addr,br,font_buffer); gbk32_32_addr+=br; w_cnt+=br; printf("font16:%.fqa4ebd8-f2f4-4fca-b286-b1bd1c1ff523r\n",(w_cnt*1.0/fno.fsize)*100); /*5. 判断文件是否结束*/ if(res!=FR_OK||br!=4096)break; } /*6. 关闭字库文件*/ f_close(&fp); }5.2 sdcard.c SD卡驱动代码#include "sdcard.h" static u8 SD_Type=0; //存放SD卡的类型 /* 函数功能:SD卡底层接口,通过SPI时序向SD卡读写一个字节 函数参数:data是要写入的数据 返 回 值:读到的数据 说明:时序是第二个上升沿采集数据 */ u8 SDCardReadWriteOneByte(u8 DataTx) { u8 DataRx; u8 i; for(i=0;i<8;i++) { SDCardSCLK(0); if(DataTx&0x80){SDCardOut(1);} else {SDCardOut(0);} DataTx<<=1; SDCardSCLK(1);//第二个上升沿采集数据 DataRx<<=1; if(SDCardInput)DataRx|=0x01; } return DataRx; } /* 函数功能:底层SD卡接口初始化 本程序SPI接口如下: PC11 片选 SDCardCS PC12 时钟 SDCardSCLK PD2 输出 SPI_MOSI--主机输出从机输入 PC8 输入 SPI_MISO--主机输入从机输出 */ void SDCardSpiInit(void) { RCC->APB2ENR|=1<<5; //使能PORTD时钟 RCC->APB2ENR|=1<<4; //使能PORTC时钟 GPIOD->CRL&=0XFFFFF0FF; GPIOD->CRL|=0X00000300; //PD2 GPIOD->ODR|=1<<2; //PD2 GPIOC->CRH&=0XFFF00FF0; GPIOC->CRH|=0X00033008; GPIOC->ODR|=0X3<<11; GPIOC->ODR|=1<<8; SDCardCS(1); } /* 函数功能:取消选择,释放SPI总线 */ void SDCardCancelCS(void) { SDCardCS(1); SDCardReadWriteOneByte(0xff);//提供额外的8个时钟 } /* 函数 功 能:选择sd卡,并且等待卡准备OK 函数返回值:0,成功;1,失败; */ u8 SDCardSelectCS(void) { SDCardCS(0); if(SDCardWaitBusy()==0)return 0;//等待成功 SDCardCancelCS(); return 1;//等待失败 } /* 函数 功 能:等待卡准备好 函数返回值:0,准备好了;其他,错误代码 */ u8 SDCardWaitBusy(void) { u32 t=0; do { if(SDCardReadWriteOneByte(0XFF)==0XFF)return 0;//OK t++; }while(t<0xFFFFFF);//等待 return 1; } /* 函数功能:等待SD卡回应 函数参数: Response:要得到的回应值 返 回 值: 0,成功得到了该回应值 其他,得到回应值失败 */ u8 SDCardGetAck(u8 Response) { u16 Count=0xFFFF;//等待次数 while((SDCardReadWriteOneByte(0XFF)!=Response)&&Count)Count--;//等待得到准确的回应 if(Count==0)return SDCard_RESPONSE_FAILURE;//得到回应失败 else return SDCard_RESPONSE_NO_ERROR;//正确回应 } /* 函数功能:从sd卡读取一个数据包的内容 函数参数: buf:数据缓存区 len:要读取的数据长度. 返回值: 0,成功;其他,失败; */ u8 SDCardRecvData(u8*buf,u16 len) { if(SDCardGetAck(0xFE))return 1;//等待SD卡发回数据起始令牌0xFE while(len--)//开始接收数据 { *buf=SDCardReadWriteOneByte(0xFF); buf++; } //下面是2个伪CRC(dummy CRC) SDCardReadWriteOneByte(0xFF); SDCardReadWriteOneByte(0xFF); return 0;//读取成功 } /* 函数功能:向sd卡写入一个数据包的内容 512字节 函数参数: buf 数据缓存区 cmd 指令 返 回 值:0表示成功;其他值表示失败; */ u8 SDCardSendData(u8*buf,u8 cmd) { u16 t; if(SDCardWaitBusy())return 1; //等待准备失效 SDCardReadWriteOneByte(cmd); if(cmd!=0XFD)//不是结束指令 { for(t=0;t<512;t++)SDCardReadWriteOneByte(buf[t]);//提高速度,减少函数传参时间 SDCardReadWriteOneByte(0xFF); //忽略crc SDCardReadWriteOneByte(0xFF); t=SDCardReadWriteOneByte(0xFF); //接收响应 if((t&0x1F)!=0x05)return 2; //响应错误 } return 0;//写入成功 } /* 函数功能:向SD卡发送一个命令 函数参数: u8 cmd 命令 u32 arg 命令参数 u8 crc crc校验值 返回值:SD卡返回的响应 */ u8 SendSDCardCmd(u8 cmd, u32 arg, u8 crc) { u8 r1; u8 Retry=0; SDCardCancelCS(); //取消上次片选 if(SDCardSelectCS())return 0XFF;//片选失效 //发送数据 SDCardReadWriteOneByte(cmd | 0x40);//分别写入命令 SDCardReadWriteOneByte(arg >> 24); SDCardReadWriteOneByte(arg >> 16); SDCardReadWriteOneByte(arg >> 8); SDCardReadWriteOneByte(arg); SDCardReadWriteOneByte(crc); if(cmd==SDCard_CMD12)SDCardReadWriteOneByte(0xff);//Skip a stuff byte when stop reading Retry=0X1F; do { r1=SDCardReadWriteOneByte(0xFF); }while((r1&0X80) && Retry--); //等待响应,或超时退出 return r1; //返回状态值 } /* 函数功能:获取SD卡的CID信息,包括制造商信息 函数参数:u8 *cid_data(存放CID的内存,至少16Byte) 返 回 值: 0:成功,1:错误 */ u8 GetSDCardCISDCardOutnfo(u8 *cid_data) { u8 r1; //发SDCard_CMD10命令,读CID r1=SendSDCardCmd(SDCard_CMD10,0,0x01); if(r1==0x00) { r1=SDCardRecvData(cid_data,16);//接收16个字节的数据 } SDCardCancelCS();//取消片选 if(r1)return 1; else return 0; } /* 函数说明: 获取SD卡的CSD信息,包括容量和速度信息 函数参数: u8 *cid_data(存放CID的内存,至少16Byte) 返 回 值: 0:成功,1:错误 */ u8 GetSDCardCSSDCardOutnfo(u8 *csd_data) { u8 r1; r1=SendSDCardCmd(SDCard_CMD9,0,0x01); //发SDCard_CMD9命令,读CSD if(r1==0) { r1=SDCardRecvData(csd_data, 16);//接收16个字节的数据 } SDCardCancelCS();//取消片选 if(r1)return 1; else return 0; } /* 函数功能:获取SD卡的总扇区数(扇区数) 返 回 值: 0表示容量检测出错,其他值表示SD卡的容量(扇区数/512字节) 说 明: 每扇区的字节数必为512字节,如果不是512字节,则初始化不能通过. */ u32 GetSDCardSectorCount(void) { u8 csd[16]; u32 Capacity; u8 n; u16 csize; if(GetSDCardCSSDCardOutnfo(csd)!=0) return 0; //取CSD信息,如果期间出错,返回0 if((csd[0]&0xC0)==0x40) //V2.00的卡,如果为SDHC卡,按照下面方式计算 { csize = csd[9] + ((u16)csd[8] << 8) + 1; Capacity = (u32)csize << 10;//得到扇区数 } else//V1.XX的卡 { n = (csd[5] & 15) + ((csd[10] & 128) >> 7) + ((csd[9] & 3) << 1) + 2; csize = (csd[8] >> 6) + ((u16)csd[7] << 2) + ((u16)(csd[6] & 3) << 10) + 1; Capacity= (u32)csize << (n - 9);//得到扇区数 } return Capacity; } /* 函数功能: 初始化SD卡 返 回 值: 非0表示初始化失败! */ u8 SDCardDeviceInit(void) { u8 r1; // 存放SD卡的返回值 u16 retry; // 用来进行超时计数 u8 buf[4]; u16 i; SDCardSpiInit(); //初始化底层IO口 for(i=0;i<10;i++)SDCardReadWriteOneByte(0XFF); //发送最少74个脉冲 retry=20; do { r1=SendSDCardCmd(SDCard_CMD0,0,0x95);//进入IDLE状态 闲置 }while((r1!=0X01) && retry--); SD_Type=0; //默认无卡 if(r1==0X01) { if(SendSDCardCmd(SDCard_CMD8,0x1AA,0x87)==1) //SD V2.0 { for(i=0;i<4;i++)buf[i]=SDCardReadWriteOneByte(0XFF); //Get trailing return value of R7 resp if(buf[2]==0X01&&buf[3]==0XAA) //卡是否支持2.7~3.6V { retry=0XFFFE; do { SendSDCardCmd(SDCard_CMD55,0,0X01); //发送SDCard_CMD55 r1=SendSDCardCmd(SDCard_CMD41,0x40000000,0X01);//发送SDCard_CMD41 }while(r1&&retry--); if(retry&&SendSDCardCmd(SDCard_CMD58,0,0X01)==0)//鉴别SD2.0卡版本开始 { for(i=0;i<4;i++)buf[i]=SDCardReadWriteOneByte(0XFF);//得到OCR值 if(buf[0]&0x40)SD_Type=SDCard_TYPE_V2HC; //检查CCS else SD_Type=SDCard_TYPE_V2; } } } else//SD V1.x/ MMC V3 { SendSDCardCmd(SDCard_CMD55,0,0X01); //发送SDCard_CMD55 r1=SendSDCardCmd(SDCard_CMD41,0,0X01); //发送SDCard_CMD41 if(r1<=1) { SD_Type=SDCard_TYPE_V1; retry=0XFFFE; do //等待退出IDLE模式 { SendSDCardCmd(SDCard_CMD55,0,0X01); //发送SDCard_CMD55 r1=SendSDCardCmd(SDCard_CMD41,0,0X01);//发送SDCard_CMD41 }while(r1&&retry--); } else//MMC卡不支持SDCard_CMD55+SDCard_CMD41识别 { SD_Type=SDCard_TYPE_MMC;//MMC V3 retry=0XFFFE; do //等待退出IDLE模式 { r1=SendSDCardCmd(SDCard_CMD1,0,0X01);//发送SDCard_CMD1 }while(r1&&retry--); } if(retry==0||SendSDCardCmd(SDCard_CMD13,512,0X01)!=0)SD_Type=SDCard_TYPE_ERR;//错误的卡 } } SDCardCancelCS(); //取消片选 if(SD_Type)return 0; //初始化成功返回0 else if(r1)return r1; //返回值错误值 return 0xaa; //其他错误 } /* 函数功能:读SD卡 函数参数: buf:数据缓存区 sector:扇区 cnt:扇区数 返回值: 0,ok;其他,失败. 说 明: SD卡一个扇区大小512字节 */ u8 SDCardReadData(u8*buf,u32 sector,u32 cnt) { u8 r1; if(SD_Type!=SDCard_TYPE_V2HC)sector<<=9;//转换为字节地址 if(cnt==1) { r1=SendSDCardCmd(SDCard_CMD17,sector,0X01);//读命令 if(r1==0) //指令发送成功 { r1=SDCardRecvData(buf,512); //接收512个字节 } }else { r1=SendSDCardCmd(SDCard_CMD18,sector,0X01);//连续读命令 do { r1=SDCardRecvData(buf,512);//接收512个字节 buf+=512; }while(--cnt && r1==0); SendSDCardCmd(SDCard_CMD12,0,0X01); //发送停止命令 } SDCardCancelCS();//取消片选 return r1;// } /* 函数功能:向SD卡写数据 函数参数: buf:数据缓存区 sector:起始扇区 cnt:扇区数 返回值: 0,ok;其他,失败. 说 明: SD卡一个扇区大小512字节 */ u8 SDCardWriteData(u8*buf,u32 sector,u32 cnt) { u8 r1; if(SD_Type!=SDCard_TYPE_V2HC)sector *= 512;//转换为字节地址 if(cnt==1) { r1=SendSDCardCmd(SDCard_CMD24,sector,0X01);//读命令 if(r1==0)//指令发送成功 { r1=SDCardSendData(buf,0xFE);//写512个字节 } } else { if(SD_Type!=SDCard_TYPE_MMC) { SendSDCardCmd(SDCard_CMD55,0,0X01); SendSDCardCmd(SDCard_CMD23,cnt,0X01);//发送指令 } r1=SendSDCardCmd(SDCard_CMD25,sector,0X01);//连续读命令 if(r1==0) { do { r1=SDCardSendData(buf,0xFC);//接收512个字节 buf+=512; }while(--cnt && r1==0); r1=SDCardSendData(0,0xFD);//接收512个字节 } } SDCardCancelCS();//取消片选 return r1;// }
  • [问题求助] 在华为lite os移植到stm32f103ze过程中发现找不到对应的.h文件
    上述的两个过程中均需要新开发版中的.h文件,但是在下载的压缩包解压之后没有发现相关的文件,希望大佬帮忙解决问题。
  • [技术干货] 基于STM32设计的拼图小游戏
    一、环境介绍MCU: STM32F103ZET6编程软件环境: keil5LCD显示屏: 正点原子的TFT 3.5寸显示屏二、运行效果三、功能介绍因为代码原来是在2.8寸屏上设计的,但是2.8寸屏摔碎了,现在手上只有块3.5寸的LCD屏。就把代码移植到3.5寸屏上显示了,目前运行的效果可以看上面的效果图。拼图游戏在小时候还是经常玩,玩法也比较简单,这里就使用STM32设计一个拼图小游戏分享出来一起学习。目前游戏是通过开发板上4按键进行控制图片的移动,完成最终的拼图效果,如果想要在上面扩充其他游戏功能都还是比较容易。当然,设计编写这个款小游戏不仅仅是为了玩游戏这么简单,主要是为了通过设计游戏的过程学习STM32编程、LCD的编程、按键、图片等等很多编程知识。做拼图这款游戏主要的硬件就是LCD显示屏和几个按键,没有其他外设,如果自己有其他STM32最小系统板有LCD显示屏的话,完全可以把代码移植过去运行。当前的代码兼容正点原子战舰开发板,可以直接在正点原子战舰直接编译下载运行。其他的开发板上运行也是一样的道理,底层替换自己LCD画点函数即可。目前的拼图的游戏工程里图片是提前通过取模软件取好模之后把数据放在工程里的,如果想要动态更加灵活的切换拼图的图片,可以加上SD卡和FATFS文件系统,把图片放在SD卡,通过按键选择指定图片切割进行拼图。代码分为3个部分:(1). LCD屏驱动代码部分(2). 按键驱动代码部分(3). 拼图的逻辑代码部分四、LCD屏驱动代码4.1 LCD介绍这款LCD显示屏的驱动芯片型号是NT35310,时序支持8080时序,普通的MCU可以采用IO模拟8080时序驱动,有硬件接口的可以采用硬件接口驱动。STM32F103ZET6的FSMC是可以输出8080时序,为了提高刷屏速度,这里代码就采用FSMC接口来驱动LCD屏。照着LCD的手册指令集,自己编写一遍LCD的初始化代码,可以深入学习一遍LCD编程原理。4.2 LCD显示屏的初始化代码示例/* 函数功能:写寄存器函数 参 数:regval:寄存器值 */ void LcdWriteReg(u16 regval) { LCD_REG=regval;//写入要写的寄存器序号 } /* 函数功能:写LCD数据 函数参数:data:要写入的值 */ void LcdWriteData(u16 data) { LCD_RAM=data; } /* 函数功能:读LCD数据 返 回 值:读到的值 */ u16 LcdReadData(void) { vu16 ram; //防止被优化 ram=LCD_RAM; return ram; } /* 函数功能:读寄存器 函数参数:LCD_Reg:寄存器地址 返 回 值:读到的数据 */ u16 LcdReadReg(u16 LCD_Reg) { LcdWriteReg(LCD_Reg); //写入要读的寄存器序号 DelayUs(5); return LcdReadData(); //返回读到的值 } /* 函数功能:开始写GRAM */ void LcdWriteRAM_Prepare(void) { LCD_REG=0X2C; } /* 函数功能:LCD写GRAM 函数参数:RGB_Code:颜色值 */ void LcdWriteRAM(u16 RGB_Code) { LCD_RAM = RGB_Code;//写十六位GRAM } /* 函数功能:设置光标位置 函数参数: Xpos:横坐标 Ypos:纵坐标 */ void LcdSetCursor(u16 Xpos, u16 Ypos) { LcdWriteReg(0X2A); LcdWriteData(Xpos>>8); LcdWriteData(Xpos&0XFF); LcdWriteReg(0X2B); LcdWriteData(Ypos>>8); LcdWriteData(Ypos&0XFF); } /* 功能:读取个某点的颜色值 参数:x,y:坐标 返回值:此点的颜色 */ u16 LcdReadPoint(u16 x,u16 y) { u16 r=0,g=0,b=0; LcdSetCursor(x,y); LcdWriteReg(0X2E); r=LcdReadData(); //dummy Read DelayUs(1); r=LcdReadData(); //实际坐标颜色 DelayUs(1); b=LcdReadData(); g=r&0XFF; //5310第一次读取的是RG的值,R在前,G在后,各占8位 g<<=8; return (((r>>11)<<11)|((g>>10)<<5)|(b>>11)); } /* 功 能: 初始化LCD屏幕 说 明: 用于3.5寸屏幕的初始化。 LCD ID:5310 */ void LcdInit(void) { RCC->AHBENR|=1<<8; //使能FSMC时钟 RCC->APB2ENR|=1<<3; //使能PORTB时钟 RCC->APB2ENR|=1<<5; //使能PORTD时钟 RCC->APB2ENR|=1<<6; //使能PORTE时钟 RCC->APB2ENR|=1<<8; //使能PORTG时钟 GPIOB->CRL&=0XFFFFFFF0; //PB0 推挽输出 背光 GPIOB->CRL|=0X00000003; //PORTD复用推挽输出 GPIOD->CRH&=0X00FFF000; GPIOD->CRH|=0XBB000BBB; GPIOD->CRL&=0XFF00FF00; GPIOD->CRL|=0X00BB00BB; //PORTE复用推挽输出 GPIOE->CRH&=0X00000000; GPIOE->CRH|=0XBBBBBBBB; GPIOE->CRL&=0X0FFFFFFF; GPIOE->CRL|=0XB0000000; //PORTG12复用推挽输出 GPIOG->CRH&=0XFFF0FFFF; GPIOG->CRH|=0X000B0000; GPIOG->CRL&=0XFFFFFFF0;//PG0->RS GPIOG->CRL|=0X0000000B; //寄存器清零 //bank1有NE1~4,每一个有一个BCR+TCR,所以总共八个寄存器。 //这里我们使用NE4 ,也就对应BTCR[6],[7]。 FSMC_Bank1->BTCR[6]=0X00000000; FSMC_Bank1->BTCR[7]=0X00000000; FSMC_Bank1E->BWTR[6]=0X00000000; //操作BCR寄存器 使用异步模式 FSMC_Bank1->BTCR[6]|=1<<12; //存储器写使能 FSMC_Bank1->BTCR[6]|=1<<14; //读写使用不同的时序 FSMC_Bank1->BTCR[6]|=1<<4; //存储器数据宽度为16bit //操作BTR寄存器 //读时序控制寄存器 FSMC_Bank1->BTCR[7]|=0<<28; //模式A FSMC_Bank1->BTCR[7]|=1<<0; //地址建立时间(ADDSET)为2个HCLK 1/36M=27ns(实际>200ns) //因为液晶驱动IC的读数据的时候,速度不能太快,尤其对1289这个IC。 FSMC_Bank1->BTCR[7]|=0XF<<8; //数据保存时间为16个HCLK //写时序控制寄存器 FSMC_Bank1E->BWTR[6]|=0<<28; //模式A FSMC_Bank1E->BWTR[6]|=0<<0; //地址建立时间(ADDSET)为1个HCLK //4个HCLK(HCLK=72M)因为液晶驱动IC的写信号脉宽,最少也得50ns。72M/4=24M=55ns FSMC_Bank1E->BWTR[6]|=3<<8; //数据保存时间为4个HCLK //使能BANK1,区域4 FSMC_Bank1->BTCR[6]|=1<<0; //使能BANK1,区域4 DelayMs(50); // delay 50 ms lcddev.id=LcdReadReg(0x0000); //读ID(9320/9325/9328/4531/4535等IC) LcdWriteReg(0XD4); lcddev.id=LcdReadData();//dummy read lcddev.id=LcdReadData();//读回0X01 lcddev.id=LcdReadData();//读回0X53 lcddev.id<<=8; lcddev.id|=LcdReadData(); //这里读回0X10 printf(" LCD ID:%x ",lcddev.id); //打印LCD ID //if(lcddev.id==0x5310) LcdWriteReg(0xED); LcdWriteData(0x01); LcdWriteData(0xFE); LcdWriteReg(0xEE); LcdWriteData(0xDE); LcdWriteData(0x21); LcdWriteReg(0xF1); LcdWriteData(0x01); LcdWriteReg(0xDF); LcdWriteData(0x10); //VCOMvoltage// LcdWriteReg(0xC4); LcdWriteData(0x8F); //5f LcdWriteReg(0xC6); LcdWriteData(0x00); LcdWriteData(0xE2); LcdWriteData(0xE2); LcdWriteData(0xE2); LcdWriteReg(0xBF); LcdWriteData(0xAA); LcdWriteReg(0xB0); LcdWriteData(0x0D); LcdWriteData(0x00); LcdWriteData(0x0D); LcdWriteData(0x00); LcdWriteData(0x11); LcdWriteData(0x00); LcdWriteData(0x19); LcdWriteData(0x00); LcdWriteData(0x21); LcdWriteData(0x00); LcdWriteData(0x2D); LcdWriteData(0x00); LcdWriteData(0x3D); LcdWriteData(0x00); LcdWriteData(0x5D); LcdWriteData(0x00); LcdWriteData(0x5D); LcdWriteData(0x00); LcdWriteReg(0xB1); LcdWriteData(0x80); LcdWriteData(0x00); LcdWriteData(0x8B); LcdWriteData(0x00); LcdWriteData(0x96); LcdWriteData(0x00); LcdWriteReg(0xB2); LcdWriteData(0x00); LcdWriteData(0x00); LcdWriteData(0x02); LcdWriteData(0x00); LcdWriteData(0x03); LcdWriteData(0x00); LcdWriteReg(0xB3); LcdWriteData(0x00); LcdWriteData(0x00); LcdWriteData(0x00); LcdWriteData(0x00); LcdWriteData(0x00); LcdWriteData(0x00); LcdWriteData(0x00); LcdWriteData(0x00); LcdWriteData(0x00); LcdWriteData(0x00); LcdWriteData(0x00); LcdWriteData(0x00); LcdWriteData(0x00); LcdWriteData(0x00); LcdWriteData(0x00); LcdWriteData(0x00); LcdWriteData(0x00); LcdWriteData(0x00); LcdWriteData(0x00); LcdWriteData(0x00); LcdWriteData(0x00); LcdWriteData(0x00); LcdWriteData(0x00); LcdWriteData(0x00); LcdWriteReg(0xB4); LcdWriteData(0x8B); LcdWriteData(0x00); LcdWriteData(0x96); LcdWriteData(0x00); LcdWriteData(0xA1); LcdWriteData(0x00); LcdWriteReg(0xB5); LcdWriteData(0x02); LcdWriteData(0x00); LcdWriteData(0x03); LcdWriteData(0x00); LcdWriteData(0x04); LcdWriteData(0x00); LcdWriteReg(0xB6); LcdWriteData(0x00); LcdWriteData(0x00); LcdWriteReg(0xB7); LcdWriteData(0x00); LcdWriteData(0x00); LcdWriteData(0x3F); LcdWriteData(0x00); LcdWriteData(0x5E); LcdWriteData(0x00); LcdWriteData(0x64); LcdWriteData(0x00); LcdWriteData(0x8C); LcdWriteData(0x00); LcdWriteData(0xAC); LcdWriteData(0x00); LcdWriteData(0xDC); LcdWriteData(0x00); LcdWriteData(0x70); LcdWriteData(0x00); LcdWriteData(0x90); LcdWriteData(0x00); LcdWriteData(0xEB); LcdWriteData(0x00); LcdWriteData(0xDC); LcdWriteData(0x00); LcdWriteReg(0xB8); LcdWriteData(0x00); LcdWriteData(0x00); LcdWriteData(0x00); LcdWriteData(0x00); LcdWriteData(0x00); LcdWriteData(0x00); LcdWriteData(0x00); LcdWriteData(0x00); LcdWriteReg(0xBA); LcdWriteData(0x24); LcdWriteData(0x00); LcdWriteData(0x00); LcdWriteData(0x00); LcdWriteReg(0xC1); LcdWriteData(0x20); LcdWriteData(0x00); LcdWriteData(0x54); LcdWriteData(0x00); LcdWriteData(0xFF); LcdWriteData(0x00); LcdWriteReg(0xC2); LcdWriteData(0x0A); LcdWriteData(0x00); LcdWriteData(0x04); LcdWriteData(0x00); LcdWriteReg(0xC3); LcdWriteData(0x3C); LcdWriteData(0x00); LcdWriteData(0x3A); LcdWriteData(0x00); LcdWriteData(0x39); LcdWriteData(0x00); LcdWriteData(0x37); LcdWriteData(0x00); LcdWriteData(0x3C); LcdWriteData(0x00); LcdWriteData(0x36); LcdWriteData(0x00); LcdWriteData(0x32); LcdWriteData(0x00); LcdWriteData(0x2F); LcdWriteData(0x00); LcdWriteData(0x2C); LcdWriteData(0x00); LcdWriteData(0x29); LcdWriteData(0x00); LcdWriteData(0x26); LcdWriteData(0x00); LcdWriteData(0x24); LcdWriteData(0x00); LcdWriteData(0x24); LcdWriteData(0x00); LcdWriteData(0x23); LcdWriteData(0x00); LcdWriteData(0x3C); LcdWriteData(0x00); LcdWriteData(0x36); LcdWriteData(0x00); LcdWriteData(0x32); LcdWriteData(0x00); LcdWriteData(0x2F); LcdWriteData(0x00); LcdWriteData(0x2C); LcdWriteData(0x00); LcdWriteData(0x29); LcdWriteData(0x00); LcdWriteData(0x26); LcdWriteData(0x00); LcdWriteData(0x24); LcdWriteData(0x00); LcdWriteData(0x24); LcdWriteData(0x00); LcdWriteData(0x23); LcdWriteData(0x00); LcdWriteReg(0xC4); LcdWriteData(0x62); LcdWriteData(0x00); LcdWriteData(0x05); LcdWriteData(0x00); LcdWriteData(0x84); LcdWriteData(0x00); LcdWriteData(0xF0); LcdWriteData(0x00); LcdWriteData(0x18); LcdWriteData(0x00); LcdWriteData(0xA4); LcdWriteData(0x00); LcdWriteData(0x18); LcdWriteData(0x00); LcdWriteData(0x50); LcdWriteData(0x00); LcdWriteData(0x0C); LcdWriteData(0x00); LcdWriteData(0x17); LcdWriteData(0x00); LcdWriteData(0x95); LcdWriteData(0x00); LcdWriteData(0xF3); LcdWriteData(0x00); LcdWriteData(0xE6); LcdWriteData(0x00); LcdWriteReg(0xC5); LcdWriteData(0x32); LcdWriteData(0x00); LcdWriteData(0x44); LcdWriteData(0x00); LcdWriteData(0x65); LcdWriteData(0x00); LcdWriteData(0x76); LcdWriteData(0x00); LcdWriteData(0x88); LcdWriteData(0x00); LcdWriteReg(0xC6); LcdWriteData(0x20); LcdWriteData(0x00); LcdWriteData(0x17); LcdWriteData(0x00); LcdWriteData(0x01); LcdWriteData(0x00); LcdWriteReg(0xC7); LcdWriteData(0x00); LcdWriteData(0x00); LcdWriteData(0x00); LcdWriteData(0x00); LcdWriteReg(0xC8); LcdWriteData(0x00); LcdWriteData(0x00); LcdWriteData(0x00); LcdWriteData(0x00); LcdWriteReg(0xC9); LcdWriteData(0x00); LcdWriteData(0x00); LcdWriteData(0x00); LcdWriteData(0x00); LcdWriteData(0x00); LcdWriteData(0x00); LcdWriteData(0x00); LcdWriteData(0x00); LcdWriteData(0x00); LcdWriteData(0x00); LcdWriteData(0x00); LcdWriteData(0x00); LcdWriteData(0x00); LcdWriteData(0x00); LcdWriteData(0x00); LcdWriteData(0x00); LcdWriteReg(0xE0); LcdWriteData(0x16); LcdWriteData(0x00); LcdWriteData(0x1C); LcdWriteData(0x00); LcdWriteData(0x21); LcdWriteData(0x00); LcdWriteData(0x36); LcdWriteData(0x00); LcdWriteData(0x46); LcdWriteData(0x00); LcdWriteData(0x52); LcdWriteData(0x00); LcdWriteData(0x64); LcdWriteData(0x00); LcdWriteData(0x7A); LcdWriteData(0x00); LcdWriteData(0x8B); LcdWriteData(0x00); LcdWriteData(0x99); LcdWriteData(0x00); LcdWriteData(0xA8); LcdWriteData(0x00); LcdWriteData(0xB9); LcdWriteData(0x00); LcdWriteData(0xC4); LcdWriteData(0x00); LcdWriteData(0xCA); LcdWriteData(0x00); LcdWriteData(0xD2); LcdWriteData(0x00); LcdWriteData(0xD9); LcdWriteData(0x00); LcdWriteData(0xE0); LcdWriteData(0x00); LcdWriteData(0xF3); LcdWriteData(0x00); LcdWriteReg(0xE1); LcdWriteData(0x16); LcdWriteData(0x00); LcdWriteData(0x1C); LcdWriteData(0x00); LcdWriteData(0x22); LcdWriteData(0x00); LcdWriteData(0x36); LcdWriteData(0x00); LcdWriteData(0x45); LcdWriteData(0x00); LcdWriteData(0x52); LcdWriteData(0x00); LcdWriteData(0x64); LcdWriteData(0x00); LcdWriteData(0x7A); LcdWriteData(0x00); LcdWriteData(0x8B); LcdWriteData(0x00); LcdWriteData(0x99); LcdWriteData(0x00); LcdWriteData(0xA8); LcdWriteData(0x00); LcdWriteData(0xB9); LcdWriteData(0x00); LcdWriteData(0xC4); LcdWriteData(0x00); LcdWriteData(0xCA); LcdWriteData(0x00); LcdWriteData(0xD2); LcdWriteData(0x00); LcdWriteData(0xD8); LcdWriteData(0x00); LcdWriteData(0xE0); LcdWriteData(0x00); LcdWriteData(0xF3); LcdWriteData(0x00); LcdWriteReg(0xE2); LcdWriteData(0x05); LcdWriteData(0x00); LcdWriteData(0x0B); LcdWriteData(0x00); LcdWriteData(0x1B); LcdWriteData(0x00); LcdWriteData(0x34); LcdWriteData(0x00); LcdWriteData(0x44); LcdWriteData(0x00); LcdWriteData(0x4F); LcdWriteData(0x00); LcdWriteData(0x61); LcdWriteData(0x00); LcdWriteData(0x79); LcdWriteData(0x00); LcdWriteData(0x88); LcdWriteData(0x00); LcdWriteData(0x97); LcdWriteData(0x00); LcdWriteData(0xA6); LcdWriteData(0x00); LcdWriteData(0xB7); LcdWriteData(0x00); LcdWriteData(0xC2); LcdWriteData(0x00); LcdWriteData(0xC7); LcdWriteData(0x00); LcdWriteData(0xD1); LcdWriteData(0x00); LcdWriteData(0xD6); LcdWriteData(0x00); LcdWriteData(0xDD); LcdWriteData(0x00); LcdWriteData(0xF3); LcdWriteData(0x00); LcdWriteReg(0xE3); LcdWriteData(0x05); LcdWriteData(0x00); LcdWriteData(0xA); LcdWriteData(0x00); LcdWriteData(0x1C); LcdWriteData(0x00); LcdWriteData(0x33); LcdWriteData(0x00); LcdWriteData(0x44); LcdWriteData(0x00); LcdWriteData(0x50); LcdWriteData(0x00); LcdWriteData(0x62); LcdWriteData(0x00); LcdWriteData(0x78); LcdWriteData(0x00); LcdWriteData(0x88); LcdWriteData(0x00); LcdWriteData(0x97); LcdWriteData(0x00); LcdWriteData(0xA6); LcdWriteData(0x00); LcdWriteData(0xB7); LcdWriteData(0x00); LcdWriteData(0xC2); LcdWriteData(0x00); LcdWriteData(0xC7); LcdWriteData(0x00); LcdWriteData(0xD1); LcdWriteData(0x00); LcdWriteData(0xD5); LcdWriteData(0x00); LcdWriteData(0xDD); LcdWriteData(0x00); LcdWriteData(0xF3); LcdWriteData(0x00); LcdWriteReg(0xE4); LcdWriteData(0x01); LcdWriteData(0x00); LcdWriteData(0x01); LcdWriteData(0x00); LcdWriteData(0x02); LcdWriteData(0x00); LcdWriteData(0x2A); LcdWriteData(0x00); LcdWriteData(0x3C); LcdWriteData(0x00); LcdWriteData(0x4B); LcdWriteData(0x00); LcdWriteData(0x5D); LcdWriteData(0x00); LcdWriteData(0x74); LcdWriteData(0x00); LcdWriteData(0x84); LcdWriteData(0x00); LcdWriteData(0x93); LcdWriteData(0x00); LcdWriteData(0xA2); LcdWriteData(0x00); LcdWriteData(0xB3); LcdWriteData(0x00); LcdWriteData(0xBE); LcdWriteData(0x00); LcdWriteData(0xC4); LcdWriteData(0x00); LcdWriteData(0xCD); LcdWriteData(0x00); LcdWriteData(0xD3); LcdWriteData(0x00); LcdWriteData(0xDD); LcdWriteData(0x00); LcdWriteData(0xF3); LcdWriteData(0x00); LcdWriteReg(0xE5); LcdWriteData(0x00); LcdWriteData(0x00); LcdWriteData(0x00); LcdWriteData(0x00); LcdWriteData(0x02); LcdWriteData(0x00); LcdWriteData(0x29); LcdWriteData(0x00); LcdWriteData(0x3C); LcdWriteData(0x00); LcdWriteData(0x4B); LcdWriteData(0x00); LcdWriteData(0x5D); LcdWriteData(0x00); LcdWriteData(0x74); LcdWriteData(0x00); LcdWriteData(0x84); LcdWriteData(0x00); LcdWriteData(0x93); LcdWriteData(0x00); LcdWriteData(0xA2); LcdWriteData(0x00); LcdWriteData(0xB3); LcdWriteData(0x00); LcdWriteData(0xBE); LcdWriteData(0x00); LcdWriteData(0xC4); LcdWriteData(0x00); LcdWriteData(0xCD); LcdWriteData(0x00); LcdWriteData(0xD3); LcdWriteData(0x00); LcdWriteData(0xDC); LcdWriteData(0x00); LcdWriteData(0xF3); LcdWriteData(0x00); LcdWriteReg(0xE6); LcdWriteData(0x11); LcdWriteData(0x00); LcdWriteData(0x34); LcdWriteData(0x00); LcdWriteData(0x56); LcdWriteData(0x00); LcdWriteData(0x76); LcdWriteData(0x00); LcdWriteData(0x77); LcdWriteData(0x00); LcdWriteData(0x66); LcdWriteData(0x00); LcdWriteData(0x88); LcdWriteData(0x00); LcdWriteData(0x99); LcdWriteData(0x00); LcdWriteData(0xBB); LcdWriteData(0x00); LcdWriteData(0x99); LcdWriteData(0x00); LcdWriteData(0x66); LcdWriteData(0x00); LcdWriteData(0x55); LcdWriteData(0x00); LcdWriteData(0x55); LcdWriteData(0x00); LcdWriteData(0x45); LcdWriteData(0x00); LcdWriteData(0x43); LcdWriteData(0x00); LcdWriteData(0x44); LcdWriteData(0x00); LcdWriteReg(0xE7); LcdWriteData(0x32); LcdWriteData(0x00); LcdWriteData(0x55); LcdWriteData(0x00); LcdWriteData(0x76); LcdWriteData(0x00); LcdWriteData(0x66); LcdWriteData(0x00); LcdWriteData(0x67); LcdWriteData(0x00); LcdWriteData(0x67); LcdWriteData(0x00); LcdWriteData(0x87); LcdWriteData(0x00); LcdWriteData(0x99); LcdWriteData(0x00); LcdWriteData(0xBB); LcdWriteData(0x00); LcdWriteData(0x99); LcdWriteData(0x00); LcdWriteData(0x77); LcdWriteData(0x00); LcdWriteData(0x44); LcdWriteData(0x00); LcdWriteData(0x56); LcdWriteData(0x00); LcdWriteData(0x23); LcdWriteData(0x00); LcdWriteData(0x33); LcdWriteData(0x00); LcdWriteData(0x45); LcdWriteData(0x00); LcdWriteReg(0xE8); LcdWriteData(0x00); LcdWriteData(0x00); LcdWriteData(0x99); LcdWriteData(0x00); LcdWriteData(0x87); LcdWriteData(0x00); LcdWriteData(0x88); LcdWriteData(0x00); LcdWriteData(0x77); LcdWriteData(0x00); LcdWriteData(0x66); LcdWriteData(0x00); LcdWriteData(0x88); LcdWriteData(0x00); LcdWriteData(0xAA); LcdWriteData(0x00); LcdWriteData(0xBB); LcdWriteData(0x00); LcdWriteData(0x99); LcdWriteData(0x00); LcdWriteData(0x66); LcdWriteData(0x00); LcdWriteData(0x55); LcdWriteData(0x00); LcdWriteData(0x55); LcdWriteData(0x00); LcdWriteData(0x44); LcdWriteData(0x00); LcdWriteData(0x44); LcdWriteData(0x00); LcdWriteData(0x55); LcdWriteData(0x00); LcdWriteReg(0xE9); LcdWriteData(0xAA); LcdWriteData(0x00); LcdWriteData(0x00); LcdWriteData(0x00); LcdWriteReg(0x00); LcdWriteData(0xAA); LcdWriteReg(0xCF); LcdWriteData(0x00); LcdWriteData(0x00); LcdWriteData(0x00); LcdWriteData(0x00); LcdWriteData(0x00); LcdWriteData(0x00); LcdWriteData(0x00); LcdWriteData(0x00); LcdWriteData(0x00); LcdWriteData(0x00); LcdWriteData(0x00); LcdWriteData(0x00); LcdWriteData(0x00); LcdWriteData(0x00); LcdWriteData(0x00); LcdWriteData(0x00); LcdWriteData(0x00); LcdWriteReg(0xF0); LcdWriteData(0x00); LcdWriteData(0x50); LcdWriteData(0x00); LcdWriteData(0x00); LcdWriteData(0x00); LcdWriteReg(0xF3); LcdWriteData(0x00); LcdWriteReg(0xF9); LcdWriteData(0x06); LcdWriteData(0x10); LcdWriteData(0x29); LcdWriteData(0x00); LcdWriteReg(0x3A); LcdWriteData(0x55); //66 LcdWriteReg(0x11); DelayMs(100); LcdWriteReg(0x29); LcdWriteReg(0x35); LcdWriteData(0x00); LcdWriteReg(0x51); LcdWriteData(0xFF); LcdWriteReg(0x53); LcdWriteData(0x2C); LcdWriteReg(0x55); LcdWriteData(0x82); LcdWriteReg(0x2c); lcddev.width=320; lcddev.height=480; LCD_LED=1; //点亮背光 } 4.3 LCD相关的绘图函数代码示例/* 函数功能:画点函数 参 数:x,y坐标 color颜色值 */ void LcdDrawPoint(u16 x,u16 y,u16 color) { LcdSetCursor(x,y); //设置光标位置 LcdWriteRAM_Prepare(); //开始写入GRAM LCD_RAM=color; } /* 函数功能:清屏函数 参 数:color颜色值 */ void LcdClear(u16 color) { u32 i; LcdSetCursor(0,0); //设置光标位置 LcdWriteRAM_Prepare(); //开始写入GRAM for(i=0;i<320*480;i++)LCD_RAM=color; } /* 函数功能:显示一个指定大小的汉字 参 数: x,y :汉字的坐标 font:取模数据 size:字体大小尺寸 */ void LcdShowFont(u16 x,u16 y,u8 *dzk,u8 size) { u8 temp,t,t1; u16 x0=x; u8 csize=(size/8+((size%8)?1:0))*(size);//得到字体一个字符对应点阵集所占的字节数 for(t=0;t0)incx=1; //设置单步方向 else if(delta_x==0)incx=0;//垂直线 else {incx=-1;delta_x=-delta_x;} if(delta_y>0)incy=1; else if(delta_y==0)incy=0;//水平线 else{incy=-1;delta_y=-delta_y;} if( delta_x>delta_y)distance=delta_x; //选取基本增量坐标轴 else distance=delta_y; for(t=0;t<=distance+1;t++ )//画线输出 { LcdDrawPoint(uRow,uCol,color);//画点 xerr+=delta_x ; yerr+=delta_y ; if(xerr>distance) { xerr-=distance; uRow+=incx; } if(yerr>distance) { yerr-=distance; uCol+=incy; } } } /* 函数功能:任意角度画直线 参 数: w :线段的长度 len:半径 c :颜色 x,y:坐标 */ #include void LcdDrawAngleLine(u32 x,u32 y,float du,u32 len,u32 w,u16 c) { int i; int x0,y0; float k=du*(3.1415926535/180); for(i=len-w;i高位在前 //画点LCD的像素是16位的,所以需要将2个8位像素合成16位像素。 p+=2; } } }五、按键驱动代码示例拼图的时候,通过4个按键控制上下左右方向的,按键的代码如下:功能 :按键的初始化 硬件连接: KEY1:PA0 --按下是高电平 KEY2:PE2 --按下是低电平 KEY3:PE3 --按下是低电平 KEY4:PE4 --按下是低电平 返回值:无 */ void KeyInit(void) { /*1. 开时钟*/ RCC->APB2ENR|=1<<2; //PA RCC->APB2ENR|=1<<6; //PE /*2. 配置GPIO口模式*/ GPIOA->CRL&=0xFFFFFFF0; GPIOA->CRL|=0x00000008; //上下拉输入模式 GPIOE->CRL&=0xFFF000FF; GPIOE->CRL|=0x00088800; //上下拉输入模式 /*3. 上下拉控制*/ //GPIOA->ODR&=~(1<<0); //PA0下拉 GPIOE->ODR|=1<<2; GPIOE->ODR|=1<<3; GPIOE->ODR|=1<<4; } /* 功 能:获取按键值 返回值: 1表示按键1按下 2表示按键2按下 3表示按键3按下 4表示按键4按下 0表示没有按键按下 实现单次按下模式(上一次按下的按键没有松开,就不做检测)和连续按下的模式。 模式: mode=1; //表示单击 mode=0; //表示连续 */ u8 GetKeyValue(u32 mode) { static int stat=0; //保存上一次按键的状态 if(mode==0)stat=0; if((KEY1||KEY2==0||KEY3==0||KEY4==0) && stat==0) { DelayMs(20); //延时消抖 stat=1; //表示上次有按键按下 if(KEY1)return 1; if(KEY2==0)return 2; if(KEY3==0)return 3; if(KEY4==0)return 4; } else { if(KEY1==0&&KEY2&&KEY3&&KEY4)stat=0; //判断所有的按键是否已经松开 } return 0; }六、拼图的逻辑代码分隔图片: 这里是按照320*240---2.8寸计算的// 0,0 60,0 120,0 180,0 // 0,80 60,80 120, 80 180,80 // 0,160 60,160, 120, 160, 180,160 // 0,240, 60,240 120, 240 180,240 //显示位置起始坐标。 u32 record[16][2]; int pic[16][2]= {//第i张图片的屏幕所在位置。 {60,160},{180,80},{0,80}, {180,0},{180,160},{180,240}, {0,160},{120,160},{60,80},{120,0}, {120,240},{120,80},{0,0},{60,240},{60,0},{0,240}}; 循环里处理图片移动逻辑:while(1) { //【1】产生随机图片ID号:0--15, 4号图片用作空白图片。 //ID号和图片首地址固定。图片所在的位置不固定。 picture_production(tu32,pic); while(1) { key=GetKeyValue(1); if(key==1) //右移动 {if(record[4][0]>0) //180,160 { number=lookingfor_blankblock(record[4][0]-60,record[4][1],tu32); show_picture_(record[4][0],record[4][1],tu32,number);//空白左边图覆盖空白 show_picture_(record[4][0]-60,record[4][1],tu32,4); //出现新空白 k=record[number][0]; record[number][0]=record[4][0]; record[4][0]=k; } } if(key==3) //左移动 {if(record[4][0]<180) //180,160 { number=lookingfor_blankblock(record[4][0]+60,record[4][1],tu32); show_picture_(record[4][0],record[4][1],tu32,number);//空白左边图覆盖空白 show_picture_(record[4][0]+60,record[4][1],tu32,4); //出现新空白 k=record[number][0]; record[number][0]=record[4][0]; record[4][0]=k; //新空白横坐标 } } if(key==2) //下移动 {if(record[4][1]>0) { number=lookingfor_blankblock(record[4][0],record[4][1]-80,tu32); show_picture_(record[4][0],record[4][1],tu32,number);//空白左边图覆盖空白 show_picture_(record[4][0],record[4][1]-80,tu32,4); //出现新空白 k=record[number][1]; record[number][1]=record[4][1]; record[4][1]=k; } } if(key==4) //上移动 { if(record[4][1]<240) { number=lookingfor_blankblock(record[4][0],record[4][1]+80,tu32); show_picture_(record[4][0],record[4][1],tu32,number);//空白左边图覆盖空白 show_picture_(record[4][0],record[4][1]+80,tu32,4); //出现新空白 k=record[number][1]; record[number][1]=record[4][1]; record[4][1]=k; } } } }
  • [技术干货] 基于STM32的录音机设计(STM32F103+VS1053B)
    一、环境介绍MCU: STM32F103C8T6开发软件: Keil5音频模块: VS1053B录音文件存储设备: SD卡,采用SPI协议驱动显示屏: SPI接口的0.96寸OLED代码风格: 采用寄存器编程,代码简洁、执行效率高、注释到位、移植方便。二、功能介绍这是基于STM32F103C8T6设计的录音机功能,支持的功能如下:(1). 按下按键1启动自动录音,默认为5秒录音一次,录音完毕自动保存在SD指定目录下。文件名称采用当前时间命名;音频文件格式采用WAV格式存储。(2). 按下按键2启动手动录音,按键按下之后开始录音,再次按下结束录音,录音完毕之后,文件也是一样的保存在SD卡里。(3). SD卡文件系统采用FAT32格式,STM32移植了FATFS开源文件系统对SD卡进行读写操作。(4). OLED显示屏用于显示当前录音机的状态: 空闲、录音、回放等状态。(5). 按下按键3,启动自动回放功能。自动扫描目录,按顺序播放录音文件。 技术介绍:(1). SD卡采用SPI协议驱动,因为对速度没有很高要求,SPI协议已经完全满足;如果要更高的速度,可以采用SDIO协议。(2). 音频模块采用VS1053B,这个芯片支持IIS和SPI协议。我这里采用的是SPI协议驱动,SPI比较简单,代码也好移植,可以很方便的移植到其他单片机上运行。VS1053功能比较强大,支持录音、解码播放。(3). 文件系统采用的是FATFS文件系统,这个文件系统功能比较完善,使用免费,支持FAT16、FAT32等格式。底层也比较好适配移植。当前,除了FATFS以外,还有很多其他的嵌入式文件系统可以选择,移植都大同小异。(4). OLED显示屏是0.96寸的。采用SPI协议驱动,主要是显示一些状态,SPI刷屏比较快,这款OLED也支持IIC接口。(5). VS1053模块上没有喇叭设备,可以适应耳机或者音箱听回放的录音。硬件与STM32的接线说明:OLED显示屏: D0----SCK-----PB14 D1----MOSI----PB13 RES—复位(低电平有效)—PB12 DC---数据和命令控制管脚—PB1 CS---片选引脚-----PA7VS1053: #define VS1053_DREQ PAin(11) //DREQ 数据请求 #define VS1053_RESET PAout(12) //RST 硬件复位--低电平有效 #define VS1053_XCS PAout(13) //XCS 片选--低电平有效 #define VS1053_XDCS PAout(14) //XDCS 用于数据片选、字节同步 #define VS1053_SCLK PAout(15) #define VS1053_OUTPUT PBout(3) #define VS1053_INPUT PBin(4)SD卡接口: 5V----5V GND---GND SPI1_MOSI---PA7 SPI1_MISO---PA6 SPI1_CS---PA4 SPI1_SCK--PA5三、使用的相关硬件STM32F103C8T6系统板:OLED显示屏:VS1053:SD卡卡槽: 四、操作说明开发板有一个复位键和一个K0按键。程序下载:程序支持三种模式:因为开发板只有一个K0按键,所以三种模式都是通过一个按键进行切换的。一个按键是通过按下的计数方式进行切换的,切换的顺序是自动录音模式、手动录音模式、回放模式。(1)自动录音模式:按下一次按键后,进入自动录音模式,自动录音模式下,录音5秒自动退出,退出后自动启动播放状态,就是播放刚才5秒录制的音频,播放过程中按下按键可以退出播放状态。(2)手动录音模式:第二次按下K0按键后,进入手动录音模式,手动录音模式下,可以长时间录音,如果要结束录音,按下K0按键即可结束;结束后自动启动播放状态,就是播放刚才录制的音频,播放过程中按下按键可以退出播放状态。(3)回放模式:第三次按下K0按键后,进入回放模式,自动扫描wav目录,进行顺序播放音频文件。播放过程中可以按下K0按键退出回放模式。 每次录音后的文件是存放在SD卡根目录下的wav目录下。 每个状态都会在OLED显示屏上显示 也会同时通过串口打印到串口调试助手终端。五、SD卡上存放的文件SD卡上有两个目录:font目录和wav目录。 font目录下存放16x16字库文件。 wav目录下存放录音的音频文件。六、部分源码6.1 VS1053.c 这是VS1053的驱动代码#include "vs1053b.h" /* 函数功能:移植接口--SPI时序读写一个字节 函数参数:data:要写入的数据 返 回 值:读到的数据 */ u8 VS1053_SPI_ReadWriteByte(u8 tx_data) { u8 rx_data=0; u8 i; for(i=0;i<8;i++) { VS1053_SCLK=0; if(tx_data&0x80){VS1053_OUTPUT=1;} else {VS1053_OUTPUT=0;} tx_data<<=1; VS1053_SCLK=1; rx_data<<=1; if(VS1053_INPUT)rx_data|=0x01; } return rx_data; } /* 函数功能:初始化VS1053的IO口 */ void VS1053_Init(void) { RCC->APB2ENR|=1<<0; AFIO->MAPR&=~(0x7<<24); //释放PA13/14/15 AFIO->MAPR|=0x4<<24; RCC->APB2ENR|=1<<2; RCC->APB2ENR|=1<<3; GPIOA->CRH&=0x00000FFF; GPIOA->CRH|=0x33338000; GPIOB->CRL&=0xFFF00FFF; GPIOB->CRL|=0x00083000; VS1053_SCLK=1; VS1053_XCS=1; VS1053_RESET=1; } /* 函数功能:软复位VS10XX */ void VS1053_SoftReset(void) { u8 retry=0; while(VS1053_DREQ==0); //等待软件复位结束 VS1053_SPI_ReadWriteByte(0Xff); //启动传输 retry=0; while(VS1053_ReadReg(SPI_MODE)!=0x0800) // 软件复位,新模式 { VS1053_WriteCmd(SPI_MODE,0x0804); // 软件复位,新模式 DelayMs(2);//等待至少1.35ms if(retry++>100)break; } while(VS1053_DREQ==0);//等待软件复位结束 retry=0; while(VS1053_ReadReg(SPI_CLOCKF)!=0X9800)//设置VS10XX的时钟,3倍频 ,1.5xADD { VS1053_WriteCmd(SPI_CLOCKF,0X9800); //设置VS10XX的时钟,3倍频 ,1.5xADD if(retry++>100)break; } DelayMs(20); } /* 函数 功 能:硬复位MP3 函数返回值:1:复位失败;0:复位成功 */ u8 VS1053_Reset(void) { u8 retry=0; VS1053_RESET=0; DelayMs(20); VS1053_XDCS=1;//取消数据传输 VS1053_XCS=1; //取消数据传输 VS1053_RESET=1; while(VS1053_DREQ==0&&retry<200)//等待DREQ为高 { retry++; DelayUs(50); } DelayMs(20); if(retry>=200)return 1; else return 0; } /* 函数功能:向VS10XX写命令 函数参数: address:命令地址 data :命令数据 */ void VS1053_WriteCmd(u8 address,u16 data) { while(VS1053_DREQ==0); //等待空闲 VS1053_XDCS=1; VS1053_XCS=0; VS1053_SPI_ReadWriteByte(VS_WRITE_COMMAND);//发送VS10XX的写命令 VS1053_SPI_ReadWriteByte(address); //地址 VS1053_SPI_ReadWriteByte(data>>8); //发送高八位 VS1053_SPI_ReadWriteByte(data); //第八位 VS1053_XCS=1; } /* 函数参数:向VS1053写数据 函数参数:data:要写入的数据 */ void VS1053_WriteData(u8 data) { VS1053_XDCS=0; VS1053_SPI_ReadWriteByte(data); VS1053_XDCS=1; } /* 函数功能:读VS1053的寄存器 函数参数:address:寄存器地址 返回值:读到的值 */ u16 VS1053_ReadReg(u8 address) { u16 temp=0; while(VS1053_DREQ==0);//非等待空闲状态 VS1053_XDCS=1; VS1053_XCS=0; VS1053_SPI_ReadWriteByte(VS_READ_COMMAND);//发送VS10XX的读命令 VS1053_SPI_ReadWriteByte(address); //地址 temp=VS1053_SPI_ReadWriteByte(0xff); //读取高字节 temp=temp<<8; temp+=VS1053_SPI_ReadWriteByte(0xff); //读取低字节 VS1053_XCS=1; return temp; } /* 函数功能:读取VS1053的RAM 函数参数:addr:RAM地址 返 回 值:读到的值 */ u16 VS1053_ReadRAM(u16 addr) { u16 res; VS1053_WriteCmd(SPI_WRAMADDR, addr); res=VS1053_ReadReg(SPI_WRAM); return res; } /* 函数功能:写VS1053的RAM 函数参数: addr:RAM地址 val:要写入的值 */ void VS1053_WriteRAM(u16 addr,u16 val) { VS1053_WriteCmd(SPI_WRAMADDR,addr); //写RAM地址 while(VS1053_DREQ==0); //等待空闲 VS1053_WriteCmd(SPI_WRAM,val); //写RAM值 } /* 函数参数:发送一次音频数据,固定为32字节 返 回 值:0,发送成功 1,本次数据未成功发送 */ u8 VS1053_SendMusicData(u8* buf) { u8 n; if(VS1053_DREQ!=0) //送数据给VS10XX { VS1053_XDCS=0; for(n=0;n<32;n++) { VS1053_SPI_ReadWriteByte(buf[n]); } VS1053_XDCS=1; }else return 1; return 0;//成功发送了 } /* 函数参数:发送一次音频数据,固定为32字节 返 回 值:0,发送成功 1,本次数据未成功发送 */ void VS1053_SendMusicByte(u8 data) { u8 n; while(VS1053_DREQ==0){} VS1053_XDCS=0; VS1053_SPI_ReadWriteByte(data); VS1053_XDCS=1; } /* 函数功能:设定VS1053播放的音量 函数参数:volx:音量大小(0~254) */ void VS1053_SetVol(u8 volx) { u16 volt=0; //暂存音量值 volt=254-volx; //取反一下,得到最大值,表示最大的表示 volt<<=8; volt+=254-volx; //得到音量设置后大小 VS1053_WriteCmd(SPI_VOL,volt);//设音量 } /*--------------------------------------录音功能-----------------------------------------------------*/ /* 函数功能:vs10xx装载patch 函数参数: patch:patch首地址 len :patch长度 */ void VS1053_LoadPatch(u16 *patch,u16 len) { u16 i; u16 addr, n, val; for(i=0;iriff.ChunkID=0X46464952; //"RIFF" wavhead->riff.ChunkSize=0; //还未确定,最后需要计算 wavhead->riff.Format=0X45564157; //"WAVE" wavhead->fmt.ChunkID=0X20746D66; //"fmt " wavhead->fmt.ChunkSize=16; //大小为16个字节 wavhead->fmt.AudioFormat=0X01; //0X01,表示PCM;0X01,表示IMA ADPCM wavhead->fmt.NumOfChannels=1; //单声道 wavhead->fmt.SampleRate=8000; //8Khz采样率 采样速率 wavhead->fmt.ByteRate=wavhead->fmt.SampleRate*2;//16位,即2个字节 wavhead->fmt.BlockAlign=2; //块大小,2个字节为一个块 wavhead->fmt.BitsPerSample=16; //16位PCM wavhead->data.ChunkID=0X61746164; //"data" wavhead->data.ChunkSize=0; //数据大小,还需要计算 } //VS1053的WAV录音有bug,这个plugin可以修正这个问题 const u16 VS1053_WavPlugin[40]=/* Compressed plugin */ { 0x0007, 0x0001, 0x8010, 0x0006, 0x001c, 0x3e12, 0xb817, 0x3e14, /* 0 */ 0xf812, 0x3e01, 0xb811, 0x0007, 0x9717, 0x0020, 0xffd2, 0x0030, /* 8 */ 0x11d1, 0x3111, 0x8024, 0x3704, 0xc024, 0x3b81, 0x8024, 0x3101, /* 10 */ 0x8024, 0x3b81, 0x8024, 0x3f04, 0xc024, 0x2808, 0x4800, 0x36f1, /* 18 */ 0x9811, 0x0007, 0x0001, 0x8028, 0x0006, 0x0002, 0x2a00, 0x040e, }; /* 函数功能:激活PCM 录音模式 函数参数: agc:0,自动增益 1024相当于1倍 512相当于0.5倍 最大值65535=64倍 */ void VS1053_RecoderInit(u16 agc) { //如果是IMA ADPCM,采样率计算公式如下: //采样率=CLKI/256*d; //假设d=0,并2倍频,外部晶振为12.288M.那么Fc=(2*12288000)/256*6=16Khz //如果是线性PCM,采样率直接就写采样值 VS1053_WriteCmd(SPI_BASS,0x0000); VS1053_WriteCmd(SPI_AICTRL0,8000); //设置采样率,设置为8Khz VS1053_WriteCmd(SPI_AICTRL1,agc); //设置增益,0,自动增益.1024相当于1倍,512相当于0.5倍,最大值65535=64倍 VS1053_WriteCmd(SPI_AICTRL2,0); //设置增益最大值,0,代表最大值65536=64X VS1053_WriteCmd(SPI_AICTRL3,6); //左通道(MIC单声道输入) VS1053_WriteCmd(SPI_CLOCKF,0X2000); //设置VS10XX的时钟,MULT:2倍频;ADD:不允许;CLK:12.288Mhz VS1053_WriteCmd(SPI_MODE,0x1804); //MIC,录音激活 DelayMs(5); //等待至少1.35ms VS1053_LoadPatch((u16*)VS1053_WavPlugin,40);//VS1053的WAV录音需要patch }6.2 SD.c 这是SD卡的驱动代码#include "sdcard.h" static u8 SD_Type=0; //存放SD卡的类型 /* 函数功能:SD卡底层接口,通过SPI时序向SD卡读写一个字节 函数参数:data是要写入的数据 返 回 值:读到的数据 */ u8 SDCardReadWriteOneByte(u8 DataTx) { u16 cnt=0; while((SPI1->SR&1<<1)==0) //等待发送区空--等待发送缓冲为空 { cnt++; if(cnt>=65530)return 0; //超时退出 u16=2个字节 } SPI1->DR=DataTx; //发送一个byte cnt=0; while((SPI1->SR&1<<0)==0) //等待接收完一个byte { cnt++; if(cnt>=65530)return 0; //超时退出 } return SPI1->DR; //返回收到的数据 } /* 函数功能:底层SD卡接口初始化 SPI1接口---SD卡接线原理 5V----5V GND---GND SPI1_MOSI---PA7 SPI1_MISO---PA6 SPI1_CS---PA4 SPI1_SCK--PA5 */ void SDCardSpiInit(void) { /*1. 开启时钟*/ RCC->APB2ENR|=1<<2; //使能PORTA时钟 /*2. 配置GPIO口模式*/ GPIOA->CRL&=0x0000FFFF; GPIOA->CRL|=0xB8B30000; /*3. 上拉*/ GPIOA->ODR|=1<<4; /*SPI1基本配置*/ RCC->APB2ENR|=1<<12; //开启SPI1时钟 RCC->APB2RSTR|=1<<12; RCC->APB2RSTR&=~(1<<12); SPI1->CR1=0X0; //清空寄存器 SPI1->CR1|=0<<15; //选择“双线双向”模式 SPI1->CR1|=0<<11; //使用8位数据帧格式进行发送/接收; SPI1->CR1|=0<<10; //全双工(发送和接收); SPI1->CR1|=1<<9; //启用软件从设备管理 SPI1->CR1|=1<<8; //NSS SPI1->CR1|=0<<7; //帧格式,先发送高位 SPI1->CR1|=0x0<<3;//当总线频率为36MHZ时,SPI速度为18MHZ,高速。 SPI1->CR1|=1<<2; //配置为主设备 SPI1->CR1|=1<<1; //空闲状态时, SCK保持高电平。 SPI1->CR1|=1<<0; //数据采样从第二个时钟边沿开始。 SPI1->CR1|=1<<6; //开启SPI设备。 } /* 函数功能:取消选择,释放SPI总线 */ void SDCardCancelCS(void) { SDCARD_CS=1; SDCardReadWriteOneByte(0xff);//提供额外的8个时钟 } /* 函数 功 能:选择sd卡,并且等待卡准备OK 函数返回值:0,成功;1,失败; */ u8 SDCardSelectCS(void) { SDCARD_CS=0; if(SDCardWaitBusy()==0)return 0;//等待成功 SDCardCancelCS(); return 1;//等待失败 } /* 函数 功 能:等待卡准备好 函数返回值:0,准备好了;其他,错误代码 */ u8 SDCardWaitBusy(void) { u32 t=0; do { if(SDCardReadWriteOneByte(0XFF)==0XFF)return 0;//OK t++; }while(t<0xFFFFFF);//等待 return 1; } /* 函数功能:等待SD卡回应 函数参数: Response:要得到的回应值 返 回 值: 0,成功得到了该回应值 其他,得到回应值失败 */ u8 SDCardGetAck(u8 Response) { u16 Count=0xFFFF;//等待次数 while((SDCardReadWriteOneByte(0XFF)!=Response)&&Count)Count--;//等待得到准确的回应 if(Count==0)return SDCard_RESPONSE_FAILURE;//得到回应失败 else return SDCard_RESPONSE_NO_ERROR;//正确回应 } /* 函数功能:从sd卡读取一个数据包的内容 函数参数: buf:数据缓存区 len:要读取的数据长度. 返回值: 0,成功;其他,失败; */ u8 SDCardRecvData(u8*buf,u16 len) { if(SDCardGetAck(0xFE))return 1;//等待SD卡发回数据起始令牌0xFE while(len--)//开始接收数据 { *buf=SDCardReadWriteOneByte(0xFF); buf++; } //下面是2个伪CRC(dummy CRC) SDCardReadWriteOneByte(0xFF); SDCardReadWriteOneByte(0xFF); return 0;//读取成功 } /* 函数功能:向sd卡写入一个数据包的内容 512字节 函数参数: buf 数据缓存区 cmd 指令 返 回 值:0表示成功;其他值表示失败; */ u8 SDCardSendData(u8*buf,u8 cmd) { u16 t; if(SDCardWaitBusy())return 1; //等待准备失效 SDCardReadWriteOneByte(cmd); if(cmd!=0XFD)//不是结束指令 { for(t=0;t<512;t++)SDCardReadWriteOneByte(buf[t]);//提高速度,减少函数传参时间 SDCardReadWriteOneByte(0xFF); //忽略crc SDCardReadWriteOneByte(0xFF); t=SDCardReadWriteOneByte(0xFF); //接收响应 if((t&0x1F)!=0x05)return 2; //响应错误 } return 0;//写入成功 } /* 函数功能:向SD卡发送一个命令 函数参数: u8 cmd 命令 u32 arg 命令参数 u8 crc crc校验值 返回值:SD卡返回的响应 */ u8 SendSDCardCmd(u8 cmd, u32 arg, u8 crc) { u8 r1; u8 Retry=0; SDCardCancelCS(); //取消上次片选 if(SDCardSelectCS())return 0XFF;//片选失效 //发送数据 SDCardReadWriteOneByte(cmd | 0x40);//分别写入命令 SDCardReadWriteOneByte(arg >> 24); SDCardReadWriteOneByte(arg >> 16); SDCardReadWriteOneByte(arg >> 8); SDCardReadWriteOneByte(arg); SDCardReadWriteOneByte(crc); if(cmd==SDCard_CMD12)SDCardReadWriteOneByte(0xff);//Skip a stuff byte when stop reading Retry=0X1F; do { r1=SDCardReadWriteOneByte(0xFF); }while((r1&0X80) && Retry--); //等待响应,或超时退出 return r1; //返回状态值 } /* 函数功能:获取SD卡的CID信息,包括制造商信息 函数参数:u8 *cid_data(存放CID的内存,至少16Byte) 返 回 值: 0:成功,1:错误 */ u8 GetSDCardCISDCardOutnfo(u8 *cid_data) { u8 r1; //发SDCard_CMD10命令,读CID r1=SendSDCardCmd(SDCard_CMD10,0,0x01); if(r1==0x00) { r1=SDCardRecvData(cid_data,16);//接收16个字节的数据 } SDCardCancelCS();//取消片选 if(r1)return 1; else return 0; } /* 函数说明: 获取SD卡的CSD信息,包括容量和速度信息 函数参数: u8 *cid_data(存放CID的内存,至少16Byte) 返 回 值: 0:成功,1:错误 */ u8 GetSDCardCSSDCardOutnfo(u8 *csd_data) { u8 r1; r1=SendSDCardCmd(SDCard_CMD9,0,0x01); //发SDCard_CMD9命令,读CSD if(r1==0) { r1=SDCardRecvData(csd_data, 16);//接收16个字节的数据 } SDCardCancelCS();//取消片选 if(r1)return 1; else return 0; } /* 函数功能:获取SD卡的总扇区数(扇区数) 返 回 值: 0表示容量检测出错,其他值表示SD卡的容量(扇区数/512字节) 说 明: 每扇区的字节数必为512字节,如果不是512字节,则初始化不能通过. */ u32 GetSDCardSectorCount(void) { u8 csd[16]; u32 Capacity; u16 csize; if(GetSDCardCSSDCardOutnfo(csd)!=0) return 0; //取CSD信息,如果期间出错,返回0 if((csd[0]&0xC0)==0x40) //SDHC卡,按照下面方式计算 { csize = csd[9] + ((u16)csd[8] << 8) + 1; Capacity = (u32)csize << 10;//得到扇区数 } return Capacity; } /* 函数功能: 初始化SD卡 返 回 值: 非0表示初始化失败! */ u8 SDCardDeviceInit(void) { u8 r1; // 存放SD卡的返回值 u16 retry; // 用来进行超时计数 u8 buf[4]; u16 i; SDCardSpiInit(); //初始化底层IO口 for(i=0;i<10;i++)SDCardReadWriteOneByte(0XFF); //发送最少74个脉冲 retry=20; do { r1=SendSDCardCmd(SDCard_CMD0,0,0x95);//进入IDLE状态 闲置 }while((r1!=0X01) && retry--); SD_Type=0; //默认无卡 if(r1==0X01) { if(SendSDCardCmd(SDCard_CMD8,0x1AA,0x87)==1) //SD V2.0 { for(i=0;i<4;i++)buf[i]=SDCardReadWriteOneByte(0XFF); if(buf[2]==0X01&&buf[3]==0XAA) //卡是否支持2.7~3.6V { retry=0XFFFE; do { SendSDCardCmd(SDCard_CMD55,0,0X01); //发送SDCard_CMD55 r1=SendSDCardCmd(SDCard_CMD41,0x40000000,0X01);//发送SDCard_CMD41 }while(r1&&retry--); if(retry&&SendSDCardCmd(SDCard_CMD58,0,0X01)==0)//鉴别SD2.0卡版本开始 { for(i=0;i<4;i++)buf[i]=SDCardReadWriteOneByte(0XFF);//得到OCR值 if(buf[0]&0x40)SD_Type=SDCard_TYPE_V2HC; //检查CCS else SD_Type=SDCard_TYPE_V2; } } } } SDCardCancelCS(); //取消片选 if(SD_Type)return 0; //初始化成功返回0 else if(r1)return r1; //返回值错误值 return 0xaa; //其他错误 } /* 函数功能:读SD卡 函数参数: buf:数据缓存区 sector:扇区 cnt:扇区数 返回值: 0,ok;其他,失败. 说 明: SD卡一个扇区大小512字节 */ u8 SDCardReadData(u8*buf,u32 sector,u32 cnt) { u8 r1; if(SD_Type!=SDCard_TYPE_V2HC)sector<<=9;//转换为字节地址 if(cnt==1) { r1=SendSDCardCmd(SDCard_CMD17,sector,0X01);//读命令 if(r1==0) //指令发送成功 { r1=SDCardRecvData(buf,512); //接收512个字节 } }else { r1=SendSDCardCmd(SDCard_CMD18,sector,0X01);//连续读命令 do { r1=SDCardRecvData(buf,512);//接收512个字节 buf+=512; }while(--cnt && r1==0); SendSDCardCmd(SDCard_CMD12,0,0X01); //发送停止命令 } SDCardCancelCS();//取消片选 return r1;// } /* 函数功能:向SD卡写数据 函数参数: buf:数据缓存区 sector:起始扇区 cnt:扇区数 返回值: 0,ok;其他,失败. 说 明: SD卡一个扇区大小512字节 */ u8 SDCardWriteData(u8*buf,u32 sector,u32 cnt) { u8 r1; if(SD_Type!=SDCard_TYPE_V2HC)sector *= 512;//转换为字节地址 if(cnt==1) { r1=SendSDCardCmd(SDCard_CMD24,sector,0X01);//读命令 if(r1==0)//指令发送成功 { r1=SDCardSendData(buf,0xFE);//写512个字节 } } else { if(SD_Type!=SDCard_TYPE_MMC) { SendSDCardCmd(SDCard_CMD55,0,0X01); SendSDCardCmd(SDCard_CMD23,cnt,0X01);//发送指令 } r1=SendSDCardCmd(SDCard_CMD25,sector,0X01);//连续读命令 if(r1==0) { do { r1=SDCardSendData(buf,0xFC);//接收512个字节 buf+=512; }while(--cnt && r1==0); r1=SDCardSendData(0,0xFD);//接收512个字节 } } SDCardCancelCS();//取消片选 return r1;// }
  • [知识分享] 烟雾、空气质量、温湿度…自己徒手做个环境检测设备
    摘要:综合了嵌入式处理技术、传感器技术、无线网络通信等技术,设计了一个基于STM32的无线环境监测系统,系统主要实现了对湿度、温度、有毒气体、烟雾浓度、空气质量等参数进行实时监测的功能。本文分享自华为云社区《基于STM32设计的环境检测设备》,作者:DS小龙哥 。1. 前言之前设计了一个版本,是本地监测的,只有本地OLED显示屏显示,没有加入无线数据传输。这篇的环境检测系统加了WIFI传输,并且设计了一款APP,接收数据显示。随着人们生活质量的提高,对于生活环境的问题,人们的关注度进一步提高,同时政府部门采取了许多措施来改善环境状况。但是总体上来说我国的环境监测技术水平比较落后,传统上的监测手段比较单一,监测数据也不够准确,耗尽了大量的人力和财力,却成效不高。针对上述缺点,当前文章综合了嵌入式处理技术、传感器技术、无线网络通信等技术,设计了一个基于STM32的无线环境监测系统,系统主要实现了对湿度、温度、有毒气体、烟雾浓度、空气质量等参数进行实时监测的功能。为了实现无线数据传输功能,采用了无线wifi技术。系统的测试分析表明系统整体数据采集性能良好,数据传输稳定性可靠,到达了预期目标。系统与传统的监测技术相比,具有监测数据准确,监测范围广,智能化高等特点。且系统具有一定的创新性,在实际的工程运用和理论研究上体现出了一定的研究价值最后通过实物的调试,各项参数及功能符合设计要求,能达到预期的目的。设计以STM32微控制器为平台,采用DHT11温湿度传感器、烟雾传感器MQ-2、易燃气体传感器MQ-4、空气质量检测传感器MQ-135对室内温湿度和危险气体进行采集。通过wifi无线网络将数据传送给微控制器,STM32微控制器处理数据后,由自带oled液晶屏显示。当室内温度达到预警值或有危险气体时,系统将会自动警报并将警报信息通过wifi网络传输给客户手机。且每隔一段时间会通过wifi自动发送监测信息到手机,从而实现对室内环境的监测及报警功能。基于STM32设计的环境检测设备视频演示地址2. 实现功能与整体框架图开发板采用STM32最小系统板,主控CPU采用STM32F103C8T6,其他传感器采用模块的形式连接到开发板。主要实现以下功能实现:1、通过DHT11温湿度传感器、烟雾传感器MQ-2、易燃气体传感器MQ-4、空气质量检测传感器MQ-135对室内温湿度和危险气体进行采集。2、通过传感器用ADC模拟数字的转换,采集到的数据显示在oled屏幕上。3、当检测到的数据超过设定的安全值时,屏幕上会显示警报。4、检测到的数据能定时通过ESP8266 wifi无线传输发送到所连接的用户的手机上,实现监测功能。系统框架图如下:3. 硬件特点介绍(1) 温湿度传感器温湿度传感器采用DHT11,这是一款直接输出数字信号的温湿度传感器;其精度湿度±5%RH, 温度±2℃,量程湿度5~95%RH, 温度-20~+60℃。通过单总线时序输出,占用的IO口也比较少,工作电压3V~5V,单片机连接控制很方便。(2) MQ系列的气体检测传感器烟雾传感器MQ-2、易燃气体传感器MQ-4、空气质量检测传感器MQ-135,这些传感器都是输出模拟信号。配置好STM32的ADC采集接口,采集数据进行处理即可。(3) ESP8266 WIFI联网的模块采用ESP8266 WIFI,ESP8266在物联网里使用非常多,有很多成熟的案例.WIFI本身也支持二次开发,默认集成的SDK支持AT指令控制,单片机可以通过串口方式控制ESP8266完成网络通信,非常方便.(4) OLED显示屏OLED显示屏采用中景园电子的0.96寸OLED,分辨率是128x64,使用的SPI引脚接口屏幕,刷屏速度很快,控制简单(5) 上位机设计手机APP和PC端没有单独设计精美的界面,只是简单的展示了数据显示。4. 核心源码4.1 DHT11温湿度代码#include "dht11.h" #include "delay.h" //复位DHT11 void DHT11_Rst(void) { DHT11_IO_OUT(); //SET OUTPUT DHT11_DQ_OUT=0; //拉低DQ DelayMs(20); //拉低至少18ms DHT11_DQ_OUT=1; //DQ=1 delay_us(30); //主机拉高20~40us } //等待DHT11的回应 //返回1:未检测到DHT11的存在 //返回0:存在 u8 DHT11_Check(void) { u8 retry=0; DHT11_IO_IN();//SET INPUT while (DHT11_DQ_IN&&retry<100)//DHT11会拉低40~80us { retry++; delay_us(1); }; if(retry>=100)return 1; else retry=0; while (!DHT11_DQ_IN&&retry<100)//DHT11拉低后会再次拉高40~80us { retry++; delay_us(1); }; if(retry>=100)return 1; return 0; } //从DHT11读取一个位 //返回值:1/0 u8 DHT11_Read_Bit(void) { u8 retry=0; while(DHT11_DQ_IN&&retry<100)//等待变为低电平 { retry++; delay_us(1); } retry=0; while(!DHT11_DQ_IN&&retry<100)//等待变高电平 { retry++; delay_us(1); } delay_us(40);//等待40us if(DHT11_DQ_IN)return 1; else return 0; } //从DHT11读取一个字节 //返回值:读到的数据 u8 DHT11_Read_Byte(void) { u8 i,dat; dat=0; for (i=0;i<8;i++) { dat<<=1; dat|=DHT11_Read_Bit(); } return dat; } //从DHT11读取一次数据 //temp:温度值(范围:0~50°) //humi:湿度值(范围:20%~90%) //返回值:0,正常;1,读取失败 u8 DHT11_Read_Data(u8 *temp,u8 *humi) { u8 buf[5]; u8 i; DHT11_Rst(); //printf("------------------------ "); if(DHT11_Check()==0) { for(i=0;i<5;i++)//读取40位数据 { buf[i]=DHT11_Read_Byte(); } if((buf[0]+buf[1]+buf[2]+buf[3])==buf[4]) { *humi=buf[0]; *temp=buf[2]; } }else return 1; return 0; } //初始化DHT11的IO口 DQ 同时检测DHT11的存在 //返回1:不存在 //返回0:存在 u8 DHT11_Init(void) { RCC->APB2ENR|=1<<2; //使能PORTG口时钟 GPIOA->CRL&=0XFF0FFFFF;//PORTG.11 推挽输出 GPIOA->CRL|=0X00300000; GPIOA->ODR|=1<<5; //输出1 DHT11_Rst(); return DHT11_Check(); }4.2 ESP8266代码#include "esp8266.h" extern u8 USART3_RX_BUF[USART3_MAX_RECV_LEN]; //接收缓冲,最大USART3_MAX_RECV_LEN字节 extern u8 USART3_TX_BUF[USART3_MAX_SEND_LEN]; //发送缓冲,最大USART3_MAX_SEND_LEN字节 extern vu16 USART3_RX_STA; //接收数据状态 /////////////////////////////////////////////////////////////////////////////////////////////////////////// //用户配置区 //连接端口号:8086,可自行修改为其他端口. const u8 portnum[]="8089"; //WIFI STA模式,设置要去连接的路由器无线参数,请根据你自己的路由器设置,自行修改. const u8 wifista_ssid[]="wbyq1"; //路由器SSID号 const u8 wifista_encryption[]="wpa2_aes"; //wpa/wpa2 aes加密方式 const u8 wifista_password[]="123456789"; //连接密码 //WIFI AP模式,模块对外的无线参数,可自行修改. const u8 wifiap_ssid[]="Cortex_M3"; //对外SSID号 const u8 wifiap_encryption[]="wpawpa2_aes"; //wpa/wpa2 aes加密方式 const u8 wifiap_password[]="12345678"; //连接密码 /* 函数功能:向ESP82668266发送命令 函数参数: cmd:发送的命令字符串 ack:期待的应答结果,如果为空,则表示不需要等待应答 waittime:等待时间(单位:10ms) 返 回 值: 0,发送成功(得到了期待的应答结果) 1,发送失败 */ u8 ESP8266_SendCmd(u8 *cmd,u8 *ack,u16 waittime) { u8 res=0; USART3_RX_STA=0; UsartStringSend(USART3,cmd);//发送命令 if(ack&&waittime) //需要等待应答 { while(--waittime) //等待倒计时 { DelayMs(10); if(USART3_RX_STA&0X8000)//接收到期待的应答结果 { if(ESP8266_CheckCmd(ack)) { res=0; //printf("cmd->ack:%s,%s ",cmd,(u8*)ack); break;//得到有效数据 } USART3_RX_STA=0; } } if(waittime==0)res=1; } return res; } /* 函数功能:ESP8266发送命令后,检测接收到的应答 函数参数:str:期待的应答结果 返 回 值:0,没有得到期待的应答结果 其他,期待应答结果的位置(str的位置) */ u8* ESP8266_CheckCmd(u8 *str) { char *strx=0; if(USART3_RX_STA&0X8000) //接收到一次数据了 { USART3_RX_BUF[USART3_RX_STA&0X7FFF]=0;//添加结束符 strx=strstr((const char*)USART3_RX_BUF,(const char*)str); //查找是否应答成功 printf("RX=%s",USART3_RX_BUF); } return (u8*)strx; } /* 函数功能:向ESP8266发送指定数据 函数参数: data:发送的数据(不需要添加回车) ack:期待的应答结果,如果为空,则表示不需要等待应答 waittime:等待时间(单位:10ms) 返 回 值:0,发送成功(得到了期待的应答结果)luojian */ u8 ESP8266_SendData(u8 *data,u8 *ack,u16 waittime) { u8 res=0; USART3_RX_STA=0; UsartStringSend(USART3,data);//发送数据 if(ack&&waittime) //需要等待应答 { while(--waittime) //等待倒计时 { DelayMs(10); if(USART3_RX_STA&0X8000)//接收到期待的应答结果 { if(ESP8266_CheckCmd(ack))break;//得到有效数据 USART3_RX_STA=0; } } if(waittime==0)res=1; } return res; } /* 函数功能:ESP8266退出透传模式 返 回 值:0,退出成功; 1,退出失败 */ u8 ESP8266_QuitTrans(void) { while((USART3->SR&0X40)==0); //等待发送空 USART3->DR='+'; DelayMs(15); //大于串口组帧时间(10ms) while((USART3->SR&0X40)==0); //等待发送空 USART3->DR='+'; DelayMs(15); //大于串口组帧时间(10ms) while((USART3->SR&0X40)==0); //等待发送空 USART3->DR='+'; DelayMs(500); //等待500ms return ESP8266_SendCmd("AT","OK",20);//退出透传判断. } /* 函数功能:获取ESP82668266模块的AP+STA连接状态 返 回 值:0,未连接;1,连接成功 */ u8 ESP8266_ApStaCheck(void) { if(ESP8266_QuitTrans())return 0; //退出透传 ESP8266_SendCmd("AT+CIPSTATUS",":",50); //发送AT+CIPSTATUS指令,查询连接状态 if(ESP8266_CheckCmd("+CIPSTATUS:0")&& ESP8266_CheckCmd("+CIPSTATUS:1")&& ESP8266_CheckCmd("+CIPSTATUS:2")&& ESP8266_CheckCmd("+CIPSTATUS:4")) return 0; else return 1; } /* 函数功能:获取ESP8266模块的连接状态 返 回 值:0,未连接;1,连接成功. */ u8 ESP8266_ConstaCheck(void) { u8 *p; u8 res; if(ESP8266_QuitTrans())return 0; //退出透传 ESP8266_SendCmd("AT+CIPSTATUS",":",50); //发送AT+CIPSTATUS指令,查询连接状态 p=ESP8266_CheckCmd("+CIPSTATUS:"); res=*p; //得到连接状态 return res; } /* 函数功能:获取ip地址 函数参数:ipbuf:ip地址输出缓存区 */ void ESP8266_GetWanip(u8* ipbuf) { u8 *p,*p1; if(ESP8266_SendCmd("AT+CIFSR ","OK",50))//获取WAN IP地址失败 { ipbuf[0]=0; return; } p=ESP8266_CheckCmd("""); p1=(u8*)strstr((const char*)(p+1),"""); *p1=0; sprintf((char*)ipbuf,"%s",p+1); } /* 函数功能:将收到的AT指令应答数据返回给电脑串口 参 数:mode:0,不清零USART3_RX_STA; 1,清零USART3_RX_STA; */ void ESP8266_AtResponse(u8 mode) { if(USART3_RX_STA&0X8000) //接收到一次数据了 { USART3_RX_BUF[USART3_RX_STA&0X7FFF]=0;//添加结束符 printf("%s",USART3_RX_BUF); //发送到串口 if(mode)USART3_RX_STA=0; } } /* 函数功能:ESP8266 AP模式+TCP服务器模式测试 */ void ESP8266_APorServer(void) { u8 p[100]; u8 ipbuf[20]; while(ESP8266_SendCmd("AT ","OK",20))//检查WIFI模块是否在线 { ESP8266_QuitTrans();//退出透传 ESP8266_SendCmd("AT+CIPMODE=0 ","OK",200); //关闭透传模式 printf("未检测到模块,正在尝试连接模块... "); DelayMs(800); } printf("ESP8266模块检测OK! "); while(ESP8266_SendCmd("ATE0 ","OK",20)); //关闭回显 printf("请用设备连接WIFI热点:%s,%s,%ss ",(u8*)wifiap_ssid,(u8*)wifiap_encryption,(u8*)wifiap_password); /*1. 设置WIFI AP模式 */ ESP8266_SendCmd("AT+CWMODE=2 ","OK",50); /*2. 重启模块 */ ESP8266_SendCmd("AT+RST ","OK",20); /*3. 延时3S等待重启成功*/ DelayMs(1000); DelayMs(1000); DelayMs(1000); /*5. 配置模块AP模式无线参数*/ sprintf((char*)p,"AT+CWSAP="%s","%s",1,4 ",wifiap_ssid,wifiap_password); ESP8266_SendCmd(p,"OK",1000); /*4. 设置多连接模式:0单连接,1多连接(服务器模式必须开启)*/ ESP8266_SendCmd("AT+CIPMUX=1 ","OK",20); /*5. 开启Server模式(0,关闭;1,打开),端口号为portnum */ sprintf((char*)p,"AT+CIPSERVER=1,%s ",(u8*)portnum); ESP8266_SendCmd(p,"OK",50); /*6. 获取当前模块的IP*/ ESP8266_GetWanip(ipbuf);// printf("IP地址:%s 端口:%s",ipbuf,(u8*)portnum); USART3_RX_STA=0; //清空串口的接收标志位 // while(1) // { // key=GetKeyVal(1);//退出测试 // if(key==1) // { // printf("退出测试! "); // ESP8266_QuitTrans(); //退出透传 // ESP8266_SendCmd("AT+CIPMODE=0","OK",20); //关闭透传模式 // break; // } // else if(key==2) //发送数据 // { // ESP8266_SendCmd("AT+CIPSEND=0,12 ","OK",200); //设置发送数据长度为12个 // ESP8266_SendData("ESP8266测试!","OK",100); //发送指定长度的数据 // DelayMs(200); // } // t++; // DelayMs(10); // if(USART3_RX_STA&0X8000) //接收到一次数据了 // { // rlen=USART3_RX_STA&0X7FFF; //得到本次接收到的数据长度 // USART3_RX_BUF[rlen]=0; //添加结束符 // printf("接收的数据: rlen=%d,%s",rlen,USART3_RX_BUF); //发送到串口 // USART3_RX_STA=0; // if(constate!=3)t=1000; //状态为还未连接,立即更新连接状态 // else t=0; //状态为已经连接了,10秒后再检查 // } // if(t==1000)//连续10秒钟没有收到任何数据,检查连接是不是还存在. // { //// constate=ESP8266_ConstaCheck();//得到连接状态 //// if(!constate)printf("连接失败! "); // t=0; // } // if((t )==0)LED2=!LED2; // ESP8266_AtResponse(1); // } }
  • [其他干货] CUDA编程(十一)Thrust:TOP K的另一种解题思路
    “利用GPU计算TOP10”这件事情不一定非要用核函数,还可以用Thrust的CUDA加速工具库:cub和Thrust其实也是可以排序的良方呢!CUDA Thrust的资料在这里:https://docs.nvidia.com/cuda/thrust/index.html我们先做个排序的尝试。首先,张小白搜到了这个:https://blog.csdn.net/qq_23123181/article/details/122116099里面有个例子,于是张小白就用自己的Nano上的Juputer做了尝试:这是用cmake编译的,有以下文件:CMakeLists.txtCMAKE_MINIMUM_REQUIRED(VERSION 3.5) PROJECT(thrust_examples) set(CMAKE_BUILD_TYPE Release) find_package(CUDA) include_directories(${CUDA_INCLUDE_DIRS}) message(STATUS "${CUDA_INCLUDE_DIRS}") message(STATUS "${CUDA_LIBRARIES}") cuda_add_executable(thrust_examples sort.cu)sort.cu这个张小白加了点打印信息,这样可以看得清楚些:#include <thrust/host_vector.h> #include <thrust/device_vector.h> #include <thrust/generate.h> #include <thrust/sort.h> #include <thrust/copy.h> #include <algorithm> #include <vector> #include <time.h> #define TOPK 20 int main(void) { thrust::host_vector<int> h_vec(10000*1000); std::generate(h_vec.begin(), h_vec.end(), rand); std::cout<< "size()=" << h_vec.size() <<std::endl; std::vector<int> vec(h_vec.size()); // h_vec->vec thrust::copy(h_vec.begin(), h_vec.end(), vec.begin()); // h_vec->d_vec thrust::device_vector<int> d_vec=h_vec; clock_t time1,time2; //sort d_vec //std::cout<< "d_vec.size()=" << d_vec.size() <<std::endl; std::cout<< "before sort d_vec..." <<std::endl; for(int i = 0; i < TOPK; ++i) { std::cout << d_vec << " "; } std::cout << std::endl; std::cout << std::endl; time1 = clock(); thrust::sort(d_vec.begin(), d_vec.end()); time2 = clock(); std::cout<<(double)(time2-time1)/CLOCKS_PER_SEC*1000<< " ms"<<std::endl; std::cout << std::endl; std::cout<< "after sort d_vec..." <<std::endl; for(int i = 0; i < TOPK; ++i) { std::cout << d_vec << " "; } std::cout << std::endl; std::cout << std::endl; //sort vec //std::cout<< "vec.size()=" << vec.size() <<std::endl; std::cout<< "before sort vec..." <<std::endl; for(int i = 0; i < TOPK; ++i) { std::cout << vec << " "; } std::cout << std::endl; std::cout << std::endl; time1 = clock(); std::sort(vec.begin(),vec.end()); time2 = clock(); std::cout<<(double)(time2-time1)/CLOCKS_PER_SEC*1000<< " ms"<<std::endl; std::cout << std::endl; std::cout<< "after sort vec..." <<std::endl; for(int i = 0; i < TOPK; ++i) { std::cout << vec << " "; } std::cout << std::endl; std::cout << std::endl; //sort h_vec //std::cout<< "h_vec.size()=" << h_vec.size() <<std::endl; std::cout<< "before sort h_vec..." <<std::endl; for(int i = 0; i < TOPK; ++i) { std::cout << h_vec << " "; } std::cout << std::endl; std::cout << std::endl; time1 = clock(); thrust::sort(h_vec.begin(), h_vec.end()); time2 = clock(); std::cout<<(double)(time2-time1)/CLOCKS_PER_SEC*1000<< " ms"<<std::endl; std::cout << std::endl; std::cout<< "after sort h_vec..." <<std::endl; for(int i = 0; i < TOPK; ++i) { std::cout << h_vec << " "; } std::cout << std::endl; return 0; }这里面分别对三种类型进行了排序:1.host_vector(thrust的)2.vector(STL的)3.device_vector(thrust的)我们先执行下,看看效果:解读一下:该代码先申请了一个host_vector类型的h_vec,并且随机生成了1000万条记录。然后分别申请了vector类型的vec和 device_vector类型的d_vec,并将值赋成跟h_vec完全一致。然后分别使用thrust::sort(d_vec.begin(), d_vec.end());std::sort(vec.begin(),vec.end());thrust::sort(h_vec.begin(), h_vec.end());分别给这三个1000万随机数排序(目前是升序)并打印出了最小的10个数(与TOP10相对应,可能应该叫BOTTOM10吧?张小白这么想。。。)其中第二个sort并非thrust库的。第一个和第三个sort用的是thrust库。从最终算出的时间结果也可以看出:标准库的sort耗时最长——2085.9msHOST上的thrust sort耗时较长——886.99msDEVICE上的thrust sort耗时最短——26.672ms。这样看起来,貌似比昨天作业中所有的测试都出色了。昨天TOP10的数据在这里:我们来把代码落实一下:那就开干吧!原代码如下:sort2.cu#include <stdio.h> #include <stdlib.h> #include <time.h> #include "error.cuh" #define BLOCK_SIZE 256 #define N 1000000 #define GRID_SIZE ((N + BLOCK_SIZE - 1) / BLOCK_SIZE) #define topk 10 __managed__ int source_array[N]; __managed__ int _1pass_results[topk * GRID_SIZE]; __managed__ int final_results[topk]; __device__ __host__ void insert_value(int* array, int k, int data) { for (int i = 0; i < k; i++) { if (array == data) { return; } } if (data < array[k - 1]) return; for (int i = k - 2; i >= 0; i--) { if (data > array) array[i + 1] = array; else { array[i + 1] = data; return; } } array[0] = data; } __global__ void top_k(int* input, int length, int* output, int k) { } void cpu_result_topk(int* input, int count, int* output) { /*for (int i = 0; i < topk; i++) { output = INT_MIN; }*/ for (int i = 0; i < count; i++) { insert_value(output, topk, input); } } void _init(int* ptr, int count) { srand((unsigned)time(NULL)); for (int i = 0; i < count; i++) ptr = rand(); } int main(int argc, char const* argv[]) { int cpu_result[topk] = { 0 }; cudaEvent_t start, stop; CHECK(cudaEventCreate(&start)); CHECK(cudaEventCreate(&stop)); //Fill input data buffer _init(source_array, N); printf("\n***********GPU RUN**************\n"); CHECK(cudaEventRecord(start)); top_k << <GRID_SIZE, BLOCK_SIZE >> > (source_array, N, _1pass_results, topk); CHECK(cudaGetLastError()); top_k << <1, BLOCK_SIZE >> > (_1pass_results, topk * GRID_SIZE, final_results, topk); CHECK(cudaGetLastError()); CHECK(cudaDeviceSynchronize()); CHECK(cudaEventRecord(stop)); CHECK(cudaEventSynchronize(stop)); float elapsed_time; CHECK(cudaEventElapsedTime(&elapsed_time, start, stop)); printf("Time = %g ms.\n", elapsed_time); CHECK(cudaEventDestroy(start)); CHECK(cudaEventDestroy(stop)); cpu_result_topk(source_array, N, cpu_result); int ok = 1; for (int i = 0; i < topk; ++i) { printf("cpu top%d: %d; gpu top%d: %d \n", i + 1, cpu_result, i + 1, final_results); if (fabs(cpu_result - final_results) > (1.0e-10)) { ok = 0; } } if (ok) { printf("Pass!!!\n"); } else { printf("Error!!!\n"); } return 0; }先将代码框架移植到cmake编译器上:CMakeLists.txtCMAKE_MINIMUM_REQUIRED(VERSION 3.5) PROJECT(thrust_examples) set(CMAKE_BUILD_TYPE Release) find_package(CUDA) include_directories(${CUDA_INCLUDE_DIRS}) message(STATUS "${CUDA_INCLUDE_DIRS}") message(STATUS "${CUDA_LIBRARIES}") cuda_add_executable(thrust_examples sort2.cu)其实很简单,将sort.cu改为sort2.cu即可。然后给sort2.cu加上sort.cu头文件:#include <stdio.h> #include <stdlib.h> #include <time.h> #include "error.cuh" #include <thrust/host_vector.h> #include <thrust/device_vector.h> #include <thrust/generate.h> #include <thrust/sort.h> #include <thrust/copy.h> #include <algorithm> #include <vector>并注释掉GPU RUN的那部分代码。并在GPU RUN的地方加入 thrust的相关代码。 printf("\n***********GPU RUN**************\n"); CHECK(cudaEventRecord(start)); //定义host_vector thrust::host_vector<int> h_vec; //遍历source_array,并赋值给host_vector for(int i= 0; i< N; i++) { h_vec.push_back(source_array); } printf("h_vec push ok!\n"); //定义device_vector,将host_vector复制到device_vector thrust::device_vector<int> d_vec=h_vec; printf("d_vec init ok!\n"); CHECK(cudaGetLastError()); //给device_vector排序 thrust::sort(d_vec.begin(), d_vec.end()); printf("d_vec sort ok!\n"); for (int i = 0; i < topk ; i++) { final_results = d_vec[vec.size()-1-i]; } printf("vec sort ok!\n");后面与原来的代码一样,就是打印CPU TOP10,以及cudaEvent_t通过计算GPU时间.我们全部显示一下:sort2.cu#include <stdio.h> #include <stdlib.h> #include <time.h> #include "error.cuh" #include <thrust/host_vector.h> #include <thrust/device_vector.h> #include <thrust/generate.h> #include <thrust/sort.h> #include <thrust/copy.h> #include <algorithm> #include <vector> #define BLOCK_SIZE 256 #define N 10000000 #define GRID_SIZE ((N + BLOCK_SIZE - 1) / BLOCK_SIZE) #define topk 10 __managed__ int source_array[N]; __managed__ int _1pass_results[topk * GRID_SIZE]; __managed__ int final_results[topk]; __device__ __host__ void insert_value(int* array, int k, int data) { for (int i = 0; i < k; i++) { if (array == data) { return; } } if (data < array[k - 1]) return; for (int i = k - 2; i >= 0; i--) { if (data > array) array[i + 1] = array; else { array[i + 1] = data; return; } } array[0] = data; } __global__ void top_k(int* input, int length, int* output, int k) { } void cpu_result_topk(int* input, int count, int* output) { /*for (int i = 0; i < topk; i++) { output = INT_MIN; }*/ for (int i = 0; i < count; i++) { insert_value(output, topk, input); } } void _init(int* ptr, int count) { srand((unsigned)time(NULL)); for (int i = 0; i < count; i++) ptr = rand(); } int main(int argc, char const* argv[]) { int cpu_result[topk] = { 0 }; cudaEvent_t start, stop; CHECK(cudaEventCreate(&start)); CHECK(cudaEventCreate(&stop)); //Fill input data buffer _init(source_array, N); printf("\n***********GPU RUN**************\n"); CHECK(cudaEventRecord(start)); //定义host_vector thrust::host_vector<int> h_vec; //遍历source_array,并赋值给host_vector for(int i= 0; i< N; i++) { h_vec.push_back(source_array); } printf("h_vec push ok!\n"); //定义device_vector,将host_vector复制到device_vector thrust::device_vector<int> d_vec=h_vec; printf("d_vec init ok!\n"); CHECK(cudaGetLastError()); //给device_vector排序 thrust::sort(d_vec.begin(), d_vec.end()); printf("d_vec sort ok!\n"); //取出倒排的10位存入final_results数组 for (int i = 0; i < topk ; i++) { final_results = d_vec[d_vec.size()-1-i]; } printf("final_results set ok!\n"); /* top_k << <GRID_SIZE, BLOCK_SIZE >> > (source_array, N, _1pass_results, topk); top_k << <1, BLOCK_SIZE >> > (_1pass_results, topk * GRID_SIZE, final_results, topk); CHECK(cudaGetLastError()); */ //CHECK(cudaDeviceSynchronize()); CHECK(cudaEventRecord(stop)); CHECK(cudaEventSynchronize(stop)); float elapsed_time; CHECK(cudaEventElapsedTime(&elapsed_time, start, stop)); CHECK(cudaEventDestroy(start)); CHECK(cudaEventDestroy(stop)); cpu_result_topk(source_array, N, cpu_result); int ok = 1; for (int i = 0; i < topk; ++i) { printf("cpu top%d: %d; gpu top%d: %d \n", i + 1, cpu_result, i + 1, final_results); if (fabs(cpu_result - final_results) > (1.0e-10)) { ok = 0; } } if (ok) { printf("Pass!!!\n"); } else { printf("Error!!!\n"); } printf("GPU Time = %g ms.\n", elapsed_time); return 0; }编译执行:执行没问题。只是,貌似确实有点耗时。主要是代码中先从source_array数组拷贝到 host_vector的h_vec,再从host_vector的h_vec拷贝到device_vector的d_vec,然后再排序的。我们仔细打印下具体时间: printf("\n***********GPU RUN**************\n"); CHECK(cudaEventRecord(start)); //定义host_vector thrust::host_vector<int> h_vec; //遍历source_array,并赋值给host_vector for(int i= 0; i< N; i++) { h_vec.push_back(source_array); } printf("h_vec push ok!\n"); CHECK(cudaGetLastError()); CHECK(cudaEventRecord(stop1)); CHECK(cudaEventSynchronize(stop1)); float elapsed_time; CHECK(cudaEventElapsedTime(&elapsed_time, start, stop1)); printf("h_vec push Time = %g ms.\n", elapsed_time); //定义device_vector,将host_vector复制到device_vector thrust::device_vector<int> d_vec=h_vec; printf("d_vec init ok!\n"); CHECK(cudaGetLastError()); CHECK(cudaEventRecord(stop2)); CHECK(cudaEventSynchronize(stop2)); CHECK(cudaEventElapsedTime(&elapsed_time, stop1, stop2)); printf("d_vec init Time = %g ms.\n", elapsed_time); //给device_vector排序 thrust::sort(d_vec.begin(), d_vec.end()); printf("d_vec sort ok!\n"); CHECK(cudaGetLastError()); CHECK(cudaEventRecord(stop3)); CHECK(cudaEventSynchronize(stop3)); CHECK(cudaEventElapsedTime(&elapsed_time, stop2, stop3)); printf("d_vec sort Time = %g ms.\n", elapsed_time); //取出倒排的10位存入final_results数组 for (int i = 0; i < topk ; i++) { final_results = d_vec[d_vec.size()-1-i]; } printf("final_results set ok!\n"); CHECK(cudaGetLastError()); CHECK(cudaEventRecord(stop4)); CHECK(cudaEventSynchronize(stop4)); CHECK(cudaEventElapsedTime(&elapsed_time, stop3, stop4)); printf("final_results set Time = %g ms.\n", elapsed_time); CHECK(cudaEventDestroy(start)); CHECK(cudaEventDestroy(stop1)); CHECK(cudaEventDestroy(stop2)); CHECK(cudaEventDestroy(stop3)); CHECK(cudaEventDestroy(stop4));重新编译执行:具体时间为:从source_array数组拷贝到 host_vector:206ms从host_vector拷贝到device_vector:89msdevice_vector排序:257ms复制结果到final_results:6ms(以上数据存在抖动的可能性)不过张小白试过想把source_array数组直接拷贝到device_vector,不过没有成功。比如将代码写出这样: float elapsed_time; printf("\n***********GPU RUN**************\n"); CHECK(cudaEventRecord(start)); //定义host_vector /* thrust::host_vector<int> h_vec; //遍历source_array,并赋值给host_vector for(int i= 0; i< N; i++) { h_vec.push_back(source_array); } printf("h_vec push ok!\n"); CHECK(cudaGetLastError()); CHECK(cudaEventRecord(stop1)); CHECK(cudaEventSynchronize(stop1)); CHECK(cudaEventElapsedTime(&elapsed_time, start, stop1)); printf("h_vec push Time = %g ms.\n", elapsed_time); */ //定义device_vector,将host_vector复制到device_vector //thrust::device_vector<int> d_vec=h_vec; thrust::device_vector<int> d_vec; //遍历source_array,并赋值给device_vector for(int i= 0; i< N; i++) { d_vec.push_back(source_array); } printf("d_vec init ok!\n"); CHECK(cudaGetLastError()); CHECK(cudaEventRecord(stop2)); CHECK(cudaEventSynchronize(stop2)); //CHECK(cudaEventElapsedTime(&elapsed_time, stop1, stop2)); CHECK(cudaEventElapsedTime(&elapsed_time, start, stop2)); printf("d_vec init Time = %g ms.\n", elapsed_time); //给device_vector排序 thrust::sort(d_vec.begin(), d_vec.end()); printf("d_vec sort ok!\n"); CHECK(cudaGetLastError()); CHECK(cudaEventRecord(stop3)); CHECK(cudaEventSynchronize(stop3)); CHECK(cudaEventElapsedTime(&elapsed_time, stop2, stop3)); printf("d_vec sort Time = %g ms.\n", elapsed_time); //取出倒排的10位存入final_results数组 for (int i = 0; i < topk ; i++) { final_results = d_vec[d_vec.size()-1-i]; } printf("final_results set ok!\n"); CHECK(cudaGetLastError()); CHECK(cudaEventRecord(stop4)); CHECK(cudaEventSynchronize(stop4)); CHECK(cudaEventElapsedTime(&elapsed_time, stop3, stop4)); printf("final_results set Time = %g ms.\n", elapsed_time); CHECK(cudaEventDestroy(start)); CHECK(cudaEventDestroy(stop1)); CHECK(cudaEventDestroy(stop2)); CHECK(cudaEventDestroy(stop3)); CHECK(cudaEventDestroy(stop4));运行的时候就直接卡死了,也不知道是什么原因:或许哪位大侠知道,可以告知我一下。
  • [技术干货] 以完善的产品体系 构建智能采血系统全线自连方案
    比起传统人工采血作业耗时费力易出错,智能采血系统优化了采血全流程、简化了医护人操作程序、降低了出错率风险率、缩短了标本流转周期、患者也获得更舒适的就医体验,可以说智能采血系统让近些年院内检验前管理环节有了重大进步。从排队叫号开始,直至患者拿取血检报告,智能采血系统中间要经过 排队叫号&mdash;&mdash;贴标&mdash;&mdash;采血&mdash;&mdash;输送&mdash;&mdash;分拣&mdash;&mdash;实验室检验&mdash;&mdash;报告 全部流程。由于智能采血系统全程对接医院信息系统HIS与实验室信息管理系统LIS,无线物联技术在其中起着重要的数据传输整合作用,支撑智能采血系统不同环节中的器械设备协同运作。智能采血系统运行过程中对无线技术有较高的要求:医疗级高度的数据安全性能全面对接院内HIS、LIS系统保证流转周期网络的稳定及时性不同设备接口的全面适配兼容智能采血系统全线设备包括智能叫号机、血管贴标机、发管机、智能采血台、分拣机、报告机等。设备种类多样,连接接口不统一,传输方式不一致,在部署无线物联解决方案时,若要兼容适配各类设备,更应该选择软硬件产品体系齐全的物联网服务提供商。自连科技拥有包括各类物联网芯片模组、无线终端设备、智能网关、物联网开发平台、云平台一体化的产品体系;同时,作为医疗器械领域物联服务领导者,自连在实验室检验仪器细分领域已推出数个成功方案,是与智能采血系统智能无线化方案天然适配的选择。自连目前拥有自主研发的产品50多类,依据采血系统运行环节中的不同设备可配置不同的产品,制定嵌入式、即插即用式、成品终端式解决方案。1、自连ALXB10、ALXB15网络桥接器是基于WiFi 5的经典智能终端产品,适用于原本具有有线接口的设备,是无需内部改造的快捷无线化升级方案。在嵌入式方案方面,自连可提供多款安全、易用、兼容性强的嵌入式物联模块/控制器产品。自连ALX8系列Wi-Fi控制器具有尺寸小,低功耗、高性能的特点,双天线MIMO设计、射频稳定性强、带宽高,特别适用于中等数据流量、稳定性要求高的设备。自连ALXC2A Combo 模组,集成 Wi-Fi 6 和 BT+BLE 5.0 功能于一体,具有无缝漫游功能和先进安全性,数据传输速率可达 1200 Mbps,适用于有高通讯速率需求的可设备。具有RS232接口的设备可选择AXL G10即插即用数据网关,实现RS232接口转Wi-Fi的功能,配合使用自主开发的FlaskLink软件,可以灵活快速地将配备RS23接口的设备连接至网络。2、在嵌入式方案方面,自连可提供多款安全、易用、兼容性强的嵌入式物联模块/控制器产品。自连ALX8系列Wi-Fi控制器具有尺寸小,低功耗、高性能的特点,双天线MIMO设计、射频稳定性强、带宽高,特别适用于中等数据流量、稳定性要求高的设备。自连ALXC2A Combo 模组,集成 Wi-Fi 6 和 BT+BLE 5.0 功能于一体,具有无缝漫游功能和先进安全性,数据传输速率可达 1200 Mbps,适用于有高通讯速率需求的可设备。3、具有RS232接口的设备可选择AXL G10即插即用数据网关,实现RS232接口转Wi-Fi的功能,配合使用自主开发的FlaskLink软件,可以灵活快速地将配备RS23接口的设备连接至网络。智能采血系统借助无线技术的赋能,让各方都发挥出更大效益,也极大促进了医院的信息化管理水平。
总条数:501 到第
上滑加载中