• [技术干货] STM32F103实现IAP在线升级应用程序
    # 一、环境介绍 **MCU:** STM32F103ZET6 **编程IDE:** Keil5.25 # 二、 IAP介绍 > IAP,全称是“In-Application Programming”,中文解释为“在程序中编程”。IAP是一种对通过微控制器的对外接口(如USART,IIC,CAN,USB,以太网接口甚至是无线射频通道)对正在运行程序的微控制器进行内部程序的更新的技术(注意这完全有别于ICP或者ISP技术)。 > > ICP(In-Circuit Programming)技术即通过在线仿真器对单片机进行程序烧写,而ISP技术则是通过单片机内置的bootloader程序引导的烧写技术。无论是ICP技术还是ISP技术,都需要有机械性的操作如连接下载线,设置跳线帽等。若产品的电路板已经层层密封在外壳中,要对其进行程序更新无疑困难重重,若产品安装于狭窄空间等难以触及的地方,更是一场灾难。但若进引入了IAP技术,则完全可以避免上述尴尬情况,而且若使用远距离或无线的数据传输方案,甚至可以实现远程编程和无线编程。这绝对是ICP或ISP技术无法做到的。某种微控制器支持IAP技术的首要前提是其必须是基于可重复编程闪存的微控制器。STM32微控制器带有可编程的内置闪存,同时STM32拥有在数量上和种类上都非常丰富的外设通信接口,因此在STM32上实现IAP技术是完全可行的。 > > 实现IAP技术的核心是一段预先烧写在单片机内部的IAP程序。这段程序主要负责与外部的上位机软件进行握手同步,然后将通过外设通信接口将来自于上位机软件的程序数据接收后写入单片机内部指定的闪存区域,然后再跳转执行新写入的程序,最终就达到了程序更新的目的。 > > 在STM32微控制器上实现IAP程序之前首先要回顾一下STM32的内部闪存组织架构和其启动过程。STM32的内部闪存地址起始于0x8000000,一般情况下,程序文件就从此地址开始写入。此外STM32是基于Cortex-M3内核的微控制器,其内部通过一张“中断向量表”来响应中断,程序启动后,将首先从“中断向量表”取出复位中断向量执行复位中断程序完成启动。而这张“中断向量表”的起始地址是0x8000004,当中断来临,STM32的内部硬件机制亦会自动将PC指针定位到“中断向量表”处,并根据中断源取出对应的中断向量执行中断服务程序。最后还需要知道关键的一点,通过修改STM32工程的链接脚本可以修改程序文件写入闪存的起始地址。 > > 在STM32微控制器上实现IAP方案,除了常规的串口接收数据以及闪存数据写入等常规操作外,还需注意STM32的启动过程和中断响应方式。 **下图显示了STM32常规的运行流程:** ![image.png](https://bbs-img.huaweicloud.com/blogs/img/20220706/1657114285706288032.png) ![image.png](https://bbs-img.huaweicloud.com/blogs/img/20220706/1657114295727781372.png) > **图解读如下:** > 1、 STM32复位后,会从地址为0x8000004处取出复位中断向量的地址,并跳转执行复位中断服务程序。 > 2、 复位中断服务程序执行的最终结果是跳转至C程序的main函数,而main函数应该是一个死循环,是一个永不返回的函数。 > 3、 在main函数执行的过程中,发生了一个中断请求,此时STM32的硬件机制会将PC指针强制指回中断向量表处。 > 4、 根据中断源进入相应的中断服务程序。 > 5、 中断服务程序执行完毕后,程序再度返回至main函数中执行。 **若在STM32中加入了IAP程序:** ![image.png](https://bbs-img.huaweicloud.com/blogs/img/20220706/1657114347153461341.png) ![image.png](https://bbs-img.huaweicloud.com/blogs/img/20220706/1657114359152285209.png) > 1、 STM32复位后,从地址为0x8000004处取出复位中断向量的地址,并跳转执行复位中断服务程序,随后跳转至IAP程序的main函数。 > > 2、 执行完IAP过程后(STM32内部多出了新写入的程序,地址始于0x8000004+N+M)跳转至新写入程序的复位向量表,取出新程序的复位中断向量的地址,并跳转执行新程序的复位中断服务程序,随后跳转至新程序的main函数。新程序的main函数应该也具有永不返回的特性。同时应该注意在STM32的内部存储空间在不同的位置上出现了2个中断向量表。 > > 3、 在新程序main函数执行的过程中,一个中断请求来临,PC指针仍会回转至地址为0x8000004中断向量表处,而并不是新程序的中断向量表,注意到这是由STM32的硬件机制决定的。 > > 4、 根据中断源跳转至对应的中断服务,注意此时是跳转至了新程序的中断服务程序中。 > > 5、 中断服务执行完毕后,返回main函数。 # 二、hex文件与bin文件区别 > Intel HEX文件是记录文本行的ASCII文本文件,在Intel HEX文件中,每一行是一个HEX记录,由十六进制数组成的机器码或者数据常量。Intel HEX文件经常被用于将程序或数据传输存储到ROM、EPROM,大多数编程器和模拟器使用Intel HEX文件。 > 很多编译器的支持生成HEX格式的烧录文件,尤其是Keil c。但是编程器能够下载的往往是BIN格式,因此HEX转BIN是每个编程器都必须支持的功能。HEX格式文件以行为单位,每行由“:”(0x3a)开始,以回车键结束(0x0d,0x0a)。行内的数据都是由两个字符表示一个16进制字节,比如”01”就表示数0x01;”0a”,就表示0x0a。对于16位的地址,则高位在前低位在后,比如地址0x010a,在HEX格式文件中就表示为字符串”010a”。 > > **hex和bin文件格式** > Hex文件,这里指的是Intel标准的十六进制文件,也就是机器代码的十六进制形式,并且是用一定文件格式的ASCII码来表示。具体格式介绍如下: Intel hex 文件常用来保存单片机或其他处理器的目标程序代码。它保存物理程序存储区中的目标代码映象。一般的编程器都支持这种格式。  hex和bin文件格式Hex文件,这里指的是Intel标准的十六进制文件,也就是机器代码的十六进制形式,并且是用一定文件格式的ASCII码来表示。具体格式介绍如下: Intel hex 文件常用来保存单片机或其他处理器的目标程序代码。它保存物理程序存储区中的目标代码映象。一般的编程器都支持这种格式。 # 三、使用Keil软件完成hex文件转bin文件 ![image.png](https://bbs-img.huaweicloud.com/blogs/img/20220706/1657114374589856141.png) ![image.png](https://bbs-img.huaweicloud.com/blogs/img/20220706/1657114383525504852.png) **选项框里的代码:** C:\app_setup\for_KEIL\ARM\ARMCC\bin\fromelf.exe --bin -o ./OBJECT/STM32_MD.bin ./OBJECT/STM32_MD.axf **解析如下:** C:\app_setup\for_KEIL\ARM\ARMCC\bin\fromelf.exe:是keil软件安装目录下的一个工具,用于生成bin --bin -o ./OBJECT/STM32_MD.bin :指定生成bin文件的目录和名称 ./OBJECT/STM32_MD.axf :指定输入的文件. 生成hex文件需要axf文件 **新工程的编译指令:** C:\Keil_v5\ARM\ARMCC\bin\fromelf.exe --bin -o ./obj/STM32HD.bin ./obj/STM32HD.axf ![image.png](https://bbs-img.huaweicloud.com/blogs/img/20220706/1657114413620573770.png) ![image.png](https://bbs-img.huaweicloud.com/blogs/img/20220706/1657114425391426279.png) 将该文件下载到STM32内置FLASH,复位开发板,即可启动程序。 # 四、 使用win hex软件将bin文件搞成数组 ![image.png](https://bbs-img.huaweicloud.com/blogs/img/20220706/1657114441287402730.png) ![image.png](https://bbs-img.huaweicloud.com/blogs/img/20220706/1657114453194158465.png) 生成数组之后,可以直接将数组编译到程序里,然后使用**STM32****内置FLASH****编程代码**,将该程序烧写到内置FLASH里,再复位开发板即可运行新的程序。 # 五、 Keil编译程序大小计算 **Program Size: Code=x RO-data=x RW-data=x ZI-data=x** **的含义** 1. Code(代码): 程序所占用的FLASH大小,存储在FLASH. \2. RO-data(只读的数据): Read-only-data,程序定义的常量,如const型,存储在FLASH中。 \3. RW-data(有初始值要求的、可读可写的数据): \4. Read-write-data,已经被初始化的变量,存储在FLASH中。初始化时RW-data从flash拷贝到SRAM。 \5. ZI-data:Zero-Init-data,未被初始化的可读写变量,存储在SRAM中。ZI-data不会被算做代码里因为不会被初始化。 ROM(Flash) size = Code + RO-data + RW-data; RAM size = RW-data + ZI-data 简单的说就是在烧写的时候是FLASH中的被占用的空间为:Code+RO Data+RW Data 程序运行的时候,芯片内部RAM使用的空间为: RW Data + ZI Data # 六、工程编译信息与堆栈信息查看 ![image.png](https://bbs-img.huaweicloud.com/blogs/img/20220706/1657114483318950484.png) 对于没有OS的程序,堆栈大小是在 startup.s 里设置的: Stack_Size EQU 0x00000800 对于使用用 uCos 的系统,OS自带任务的堆栈,在 os_cfg.h 里定义: ```cpp /* ——————— TASK STACK SIZE ———————- */ #define OS_TASK_TMR_STK_SIZE 128 /* Timer task stack size (# of OS_STK wide entries) */ #define OS_TASK_STAT_STK_SIZE 128 /* Statistics task stack size (# of OS_STK wide entries) */ #define OS_TASK_IDLE_STK_SIZE 128 /* Idle task stack size (# of OS_STK wide entries) */ ``` **用户程序的任务堆栈,在 app_cfg.h 里定义:** ```cpp #define APP_TASK_MANAGER_STK_SIZE 512 #define APP_TASK_GSM_STK_SIZE 512 #define APP_TASK_OBD_STK_SIZE 512 #define OS_PROBE_TASK_STK_SIZE 128 ``` **总结:** 1, 合理设置堆栈很重要 2, 多种方法结合,相互核对、校验 3, 尽量避免大数组,如果一定要用,尽量定义为 全局变量,使其不占用堆栈空间, 如果函数有重入可能性,则要注意保护。 # 七、实现STM32在线升级程序 ## 7.1 升级的思路与步骤 \1. 首先得完成STM32内置FLASH编程操作 \2. 将(升级的程序)新的程序编译生成bin文件(编译之前需要在Keil软件里设置FLASH的起始位置) \3. 创建一个专门用于升级的boot程序(**IAP Bootloader)** \4. 使用网络、串口、SD卡等方式接收到bin文件,再将bin文件烧写到STM32内置FLASH里 \5. 设置主堆栈指针 \6. 将用户代码区第二个字(**第4****个字节**)为程序开始地址(强制转为函数指针) \7. **执行函数,进行程序跳转** ## 7.2 待升级的程序FLASH起始设置 **Bootloader** 的程序大小先固定为: 20KB,最好是越小越好,可以预留更加多的空间给APP程序使用。 20KB----->20480Byte-----> 0x5000 STM32内置FLASH闪存的起始地址是: 0x08000000 ,大小是512KB。 现在将内置FLASH闪存前20KB的空间留给**Bootloader**程序使用,后面剩下的空间就给APP程序使用。 APP程序的起始位置就可以设置为: 0x08000000+ 0x5000=0x08005000 剩余的大小就是: 512KB-20KB=492KB------>503808Byte-------->0x7B000 **设置FLASH的起始位置(APP主程序):** ![image.png](https://bbs-img.huaweicloud.com/blogs/img/20220706/1657114501954139742.png) **中断向量表偏移量设置** ![image.png](https://bbs-img.huaweicloud.com/blogs/img/20220706/1657114523843125518.png) **设置编译bin文件** ![image.png](https://bbs-img.huaweicloud.com/blogs/img/20220706/1657114535511383593.png) ![image.png](https://bbs-img.huaweicloud.com/blogs/img/20220706/1657114548222203324.png) ## 7.3 Bootloader的程序设置 ```cpp //设置写入的地址,必须偶数,因为数据读写都是按照2个字节进行 #define FLASH_APP_ADDR 0x08005000 //应用程序存放到FLASH中的起始地址 int main() { printf("UART1 OK.....\n"); printf("进入IAP Bootloader程序!\n"); while(1) { key=KEY_Scanf(); if(key==1) //KEY1按下,写入STM32 FLASH { printf("正在更新IAP程序...............\n"); iap_write_appbin(FLASH_APP_ADDR,(u8*)app_bin_data,sizeof(app_bin_data));//烧写新的程序到内置FLASH printf("程序更新成功....\n"); iap_load_app(FLASH_APP_ADDR);//执行FLASH APP代码 } } } /* 函数功能:跳转到应用程序段 appxaddr:用户代码起始地址. */ typedef void (*iap_function)(void); //定义一个函数类型的参数. void IAP_LoadApp(u32 app_addr) { //给函数指针赋值合法地址 jump2app=(iap_function)*(vu32*)(app_addr+4);//用户代码区第二个字为程序开始地址(复位地址) __set_MSP(*(vu32*)app_addr); //设置主堆栈指针 jump2app(); //跳转到APP. } ``` ![image.png](https://bbs-img.huaweicloud.com/blogs/img/20220706/1657114560110736229.png)
  • [技术干货] STM32F103ZE+SHT30检测环境温度与湿度(IIC模拟时序)
    # 一、环境介绍 **工程编译软件:** keil5 **温湿度传感器:** SHT30 **MCU :** STM32F103ZET6 程序采用模块化编程,iic时序为一个模块(iic.c 和 iic.h),SHT30为一个模块(sht30.c 和 sht30.h);IIC时序采用模拟时序方式实现,IO口都采用宏定义方式,方便快速移植到其他平台使用。 # 二、SHT30介绍 ![image.png](https://bbs-img.huaweicloud.com/blogs/img/20220706/1657113884956150438.png) ![image.png](https://bbs-img.huaweicloud.com/blogs/img/20220706/1657113895132320937.png) **特点:** \1. 湿度和温度传感器 \2. 完全校准、线性化和温度 \3. 补偿数字输出,宽电源电压范围,从2.4 V到5.5 V \4. I2C接口,通信速度高达1MHz和两个用户可选地址 \5. 典型精度 +- 2%相对湿度和+- 0.3°C \6. 启动和测量时间非常快 \7. 微型8针DFN封装 ![image.png](https://bbs-img.huaweicloud.com/blogs/img/20220706/1657113912827473292.png) ![image.png](https://bbs-img.huaweicloud.com/blogs/img/20220706/1657113925478350571.png) **这是SHT30的****7位****IIC设备地址:** ![image.png](https://bbs-img.huaweicloud.com/blogs/img/20220706/1657113937487229248.png) # 三、设备运行效果 这是串口打印的数据: ![image.png](https://bbs-img.huaweicloud.com/blogs/img/20220706/1657113950398353811.png) ![image.png](https://bbs-img.huaweicloud.com/blogs/img/20220706/1657113961269291623.png) ![image.png](https://bbs-img.huaweicloud.com/blogs/img/20220706/1657113970751157683.png) # 四、设备代码 ## 4.1 main.c ```cpp #include "stm32f10x.h" #include "led.h" #include "delay.h" #include "key.h" #include "usart.h" #include "iic.h" #include "sht3x.h" int main() { float Humidity; float Temperature; USART1_Init(115200); USART1_Printf("设备运行正常....\r\n"); IIC_Init(); Init_SHT30(); while(1) { //读取温湿度 SHT3x_ReadData(&Humidity,&Temperature); USART1_Printf("温度:%0.2f,湿度:%0.2f\r\n",Temperature,Humidity); delay_ms(1000); } } ``` ## 4.2 sht30.c ```cpp #include "sht3x.h" const int16_t POLYNOMIAL = 0x131; /*************************************************************** * 函数名称: SHT30_reset * 说 明: SHT30复位 * 参 数: 无 * 返 回 值: 无 ***************************************************************/ void SHT30_reset(void) { u8 r_s; IIC_Start(); //发送起始信号 IIC_WriteOneByteData(SHT30_AddrW); //写设备地址 r_s=IIC_GetACK();//获取应答 if(r_s)printf("Init_SHT30_error:1\r\n"); IIC_WriteOneByteData(0x30); //写数据 r_s=IIC_GetACK();//获取应答 if(r_s)printf("Init_SHT30_error:2\r\n"); IIC_WriteOneByteData(0xA2); //写数据 r_s=IIC_GetACK();//获取应答 if(r_s)printf("Init_SHT30_error:3\r\n"); IIC_Stop(); //停止信号 delay_ms(50); } /*************************************************************** * 函数名称: Init_SHT30 * 说 明: 初始化SHT30,设置测量周期 * 参 数: 无 * 返 回 值: 无 ***************************************************************/ void Init_SHT30(void) { u8 r_s; IIC_Start(); //发送起始信号 IIC_WriteOneByteData(SHT30_AddrW); //写设备地址 r_s=IIC_GetACK();//获取应答 if(r_s)printf("Init_SHT30_error:1\r\n"); IIC_WriteOneByteData(0x22); //写数据 r_s=IIC_GetACK();//获取应答 if(r_s)printf("Init_SHT30_error:2\r\n"); IIC_WriteOneByteData(0x36); //写数据 r_s=IIC_GetACK();//获取应答 if(r_s)printf("Init_SHT30_error:3\r\n"); IIC_Stop(); //停止信号 delay_ms(200); } /*************************************************************** * 函数名称: SHT3x_CheckCrc * 说 明: 检查数据正确性 * 参 数: data:读取到的数据 nbrOfBytes:需要校验的数量 checksum:读取到的校对比验值 * 返 回 值: 校验结果,0-成功 1-失败 ***************************************************************/ u8 SHT3x_CheckCrc(char data[], char nbrOfBytes, char checksum) { char crc = 0xFF; char bit = 0; char byteCtr ; //calculates 8-Bit checksum with given polynomial for(byteCtr = 0; byteCtr nbrOfBytes; ++byteCtr) { crc ^= (data[byteCtr]); for ( bit = 8; bit > 0; --bit) { if (crc & 0x80) crc = (crc 1) ^ POLYNOMIAL; else crc = (crc 1); } } if(crc != checksum) return 1; else return 0; } /*************************************************************** * 函数名称: SHT3x_CalcTemperatureC * 说 明: 温度计算 * 参 数: u16sT:读取到的温度原始数据 * 返 回 值: 计算后的温度数据 ***************************************************************/ float SHT3x_CalcTemperatureC(unsigned short u16sT) { float temperatureC = 0; // variable for result u16sT &= ~0x0003; // clear bits [1..0] (status bits) //-- calculate temperature [℃] -- temperatureC = (175 * (float)u16sT / 65535 - 45); //T = -45 + 175 * rawValue / (2^16-1) return temperatureC; } /*************************************************************** * 函数名称: SHT3x_CalcRH * 说 明: 湿度计算 * 参 数: u16sRH:读取到的湿度原始数据 * 返 回 值: 计算后的湿度数据 ***************************************************************/ float SHT3x_CalcRH(unsigned short u16sRH) { float humidityRH = 0; // variable for result u16sRH &= ~0x0003; // clear bits [1..0] (status bits) //-- calculate relative humidity [%RH] -- humidityRH = (100 * (float)u16sRH / 65535); // RH = rawValue / (2^16-1) * 10 return humidityRH; } //读取温湿度数据 void SHT3x_ReadData(float *Humidity,float *Temperature) { char data[3]; //data array for checksum verification u8 SHT3X_Data_Buffer[6]; //byte 0,1 is temperature byte 4,5 is humidity u8 r_s=0; u8 i; unsigned short tmp = 0; uint16_t dat; IIC_Start(); //发送起始信号 IIC_WriteOneByteData(SHT30_AddrW); //写设备地址 r_s=IIC_GetACK();//获取应答 if(r_s)printf("SHT3X_error:1\r\n"); IIC_WriteOneByteData(0xE0); //写数据 r_s=IIC_GetACK();//获取应答 if(r_s)printf("SHT3X_error:2\r\n"); IIC_WriteOneByteData(0x00); //写数据 r_s=IIC_GetACK();//获取应答 if(r_s)printf("SHT3X_error:3\r\n"); //IIC_Stop(); //停止信号 // DelayMs(30); //等待 //读取sht30传感器数据 IIC_Start(); //发送起始信号 IIC_WriteOneByteData(SHT30_AddrR); r_s=IIC_GetACK();//获取应答 if(r_s)printf("SHT3X_error:7\r\n"); for(i=0;i6;i++) { SHT3X_Data_Buffer[i]=IIC_ReadOneByteData(); //接收数据 if(i==5) { IIC_SendACK(1); //发送非应答信号 break; } IIC_SendACK(0); //发送应答信号 } IIC_Stop(); //停止信号 // /* check tem */ data[0] = SHT3X_Data_Buffer[0]; data[1] = SHT3X_Data_Buffer[1]; data[2] = SHT3X_Data_Buffer[2]; tmp=SHT3x_CheckCrc(data, 2, data[2]); if( !tmp ) /* value is ture */ { dat = ((uint16_t)data[0] 8) | data[1]; *Temperature = SHT3x_CalcTemperatureC( dat ); } // /* check humidity */ data[0] = SHT3X_Data_Buffer[3]; data[1] = SHT3X_Data_Buffer[4]; data[2] = SHT3X_Data_Buffer[5]; tmp=SHT3x_CheckCrc(data, 2, data[2]); if( !tmp ) /* value is ture */ { dat = ((uint16_t)data[0] 8) | data[1]; *Humidity = SHT3x_CalcRH( dat ); } } ``` ## 4.3 iic.c ```cpp #include "iic.h" /* 函数功能:IIC接口初始化 硬件连接: SDA:PB7 SCL:PB6 */ void IIC_Init(void) { RCC->APB2ENR|=13;//PB GPIOB->CRL&=0x00FFFFFF; GPIOB->CRL|=0x33000000; GPIOB->ODR|=0x36; } /* 函数功能:IIC总线起始信号 */ void IIC_Start(void) { IIC_SDA_OUTMODE(); //初始化SDA为输出模式 IIC_SDA_OUT=1; //数据线拉高 IIC_SCL=1; //时钟线拉高 DelayUs(4); //电平保持时间 IIC_SDA_OUT=0; //数据线拉低 DelayUs(4); //电平保持时间 IIC_SCL=0; //时钟线拉低 } /* 函数功能:IIC总线停止信号 */ void IIC_Stop(void) { IIC_SDA_OUTMODE(); //初始化SDA为输出模式 IIC_SDA_OUT=0; //数据线拉低 IIC_SCL=0; //时钟线拉低 DelayUs(4); //电平保持时间 IIC_SCL=1; //时钟线拉高 DelayUs(4); //电平保持时间 IIC_SDA_OUT=1; //数据线拉高 } /* 函数功能:获取应答信号 返 回 值:1表示失败,0表示成功 */ u8 IIC_GetACK(void) { u8 cnt=0; IIC_SDA_INPUTMODE();//初始化SDA为输入模式 IIC_SDA_OUT=1; //数据线上拉 DelayUs(2); //电平保持时间 IIC_SCL=0; //时钟线拉低,告诉从机,主机需要数据 DelayUs(2); //电平保持时间,等待从机发送数据 IIC_SCL=1; //时钟线拉高,告诉从机,主机现在开始读取数据 while(IIC_SDA_IN) //等待从机应答信号 { cnt++; if(cnt>250)return 1; } IIC_SCL=0; //时钟线拉低,告诉从机,主机需要数据 return 0; } /* 函数功能:主机向从机发送应答信号 函数形参:0表示应答,1表示非应答 */ void IIC_SendACK(u8 stat) { IIC_SDA_OUTMODE(); //初始化SDA为输出模式 IIC_SCL=0; //时钟线拉低,告诉从机,主机需要发送数据 if(stat)IIC_SDA_OUT=1; //数据线拉高,发送非应答信号 else IIC_SDA_OUT=0; //数据线拉低,发送应答信号 DelayUs(2); //电平保持时间,等待时钟线稳定 IIC_SCL=1; //时钟线拉高,告诉从机,主机数据发送完毕 DelayUs(2); //电平保持时间,等待从机接收数据 IIC_SCL=0; //时钟线拉低,告诉从机,主机需要数据 } /* 函数功能:IIC发送1个字节数据 函数形参:将要发送的数据 */ void IIC_WriteOneByteData(u8 data) { u8 i; IIC_SDA_OUTMODE(); //初始化SDA为输出模式 IIC_SCL=0; //时钟线拉低,告诉从机,主机需要发送数据 for(i=0;i8;i++) { if(data&0x80)IIC_SDA_OUT=1; //数据线拉高,发送1 else IIC_SDA_OUT=0; //数据线拉低,发送0 IIC_SCL=1; //时钟线拉高,告诉从机,主机数据发送完毕 DelayUs(2); //电平保持时间,等待从机接收数据 IIC_SCL=0; //时钟线拉低,告诉从机,主机需要发送数据 DelayUs(2); //电平保持时间,等待时钟线稳定 data=1; //先发高位 } } /* 函数功能:IIC接收1个字节数据 返 回 值:收到的数据 */ u8 IIC_ReadOneByteData(void) { u8 i,data; IIC_SDA_INPUTMODE();//初始化SDA为输入模式 for(i=0;i8;i++) { IIC_SCL=0; //时钟线拉低,告诉从机,主机需要数据 DelayUs(2); //电平保持时间,等待从机发送数据 IIC_SCL=1; //时钟线拉高,告诉从机,主机现在正在读取数据 data=1; if(IIC_SDA_IN)data|=0x01; DelayUs(2); //电平保持时间,等待时钟线稳定 } IIC_SCL=0; //时钟线拉低,告诉从机,主机需要数据 (必须拉低,否则将会识别为停止信号) return data; } ```
  • [技术干货] STM32+MPU6050设计便携式Mini桌面时钟(自动调整时间显示方向)
    # 一、环境介绍 **MCU:** STM32F103C8T6 **姿态传感器:** MPU6050 **OLED显示屏:** 0.96寸SPI接口OLED **温度传感器:** DS18B20 **编译软件:** keil5 # 二、功能介绍 时钟可以根据MPU6050测量的姿态自动调整显示画面方向,也就是倒着拿、横着拿、反着拿都可以让时间显示是正对着自己的,时间支持自己调整,支持串口校准。可以按键切换页面查看环境温度显示。 ![image.png](https://bbs-img.huaweicloud.com/blogs/img/20220706/1657110843171526348.png) ![image.png](https://bbs-img.huaweicloud.com/blogs/img/20220706/1657110858581444315.png) ![image.png](https://bbs-img.huaweicloud.com/blogs/img/20220706/1657110879360199091.png) **支持串口时间校准:** ![image.png](https://bbs-img.huaweicloud.com/blogs/img/20220706/1657110895617699325.png) ![image.png](https://bbs-img.huaweicloud.com/blogs/img/20220706/1657110907922982083.png) # 三、核心代码 ![image.png](https://bbs-img.huaweicloud.com/blogs/img/20220706/1657110919311907358.png) ## 3.1 main.c ```cpp #include "stm32f10x.h" #include "beep.h" #include "delay.h" #include "led.h" #include "key.h" #include "sys.h" #include "usart.h" #include #include #include "exti.h" #include "timer.h" #include "rtc.h" #include "wdg.h" #include "ds18b20.h" #include "oled.h" #include "fontdata.h" #include "adc.h" #include "FunctionConfig.h" #include "mpu6050.h" #include "inv_mpu.h" #include "inv_mpu_dmp_motion_driver.h" /* 函数功能: 绘制时钟表盘框架 */ void DrawTimeFrame(void) { u8 i; OLED_Circle(32,32,31);//画外圆 OLED_Circle(32,32,1); //画中心圆 //画刻度 for(i=0;i60;i++) { if(i%5==0)OLED_DrawAngleLine(32,32,6*i,31,3,1); } OLED_RefreshGRAM(); //刷新数据到OLED屏幕 } /* 函数功能: 更新时间框架显示,在RTC中断里调用 */ char TimeBuff[20]; void Update_FrameShow(void) { /*1. 绘制秒针、分针、时针*/ OLED_DrawAngleLine2(32,32,rtc_clock.sec*6-6-90,27,0);//清除之前的秒针 OLED_DrawAngleLine2(32,32,rtc_clock.sec*6-90,27,1); //画秒针 OLED_DrawAngleLine2(32,32,rtc_clock.min*6-6-90,24,0); OLED_DrawAngleLine2(32,32,rtc_clock.min*6-90,24,1); OLED_DrawAngleLine2(32,32,rtc_clock.hour*30-6-90,21,0); OLED_DrawAngleLine2(32,32,rtc_clock.hour*30-90,21,1); //绘制电子钟时间 sprintf(TimeBuff,"%d",rtc_clock.year); OLED_ShowString(65,16*0,16,TimeBuff); //年份字符串 OLED_ShowChineseFont(66+32,16*0,16,4); //显示年 sprintf(TimeBuff,"%d/%d",rtc_clock.mon,rtc_clock.day); OLED_ShowString(75,16*1,16,TimeBuff); //月 if(rtc_clock.sec==0)OLED_ShowString(65,16*2,16," "); //清除多余的数据 sprintf(TimeBuff,"%d:%d:%d",rtc_clock.hour,rtc_clock.min,rtc_clock.sec); OLED_ShowString(65,16*2,16,TimeBuff); //秒 //显示星期 OLED_ShowChineseFont(70,16*3,16,5); //星 OLED_ShowChineseFont(70+16,16*3,16,6); //期 OLED_ShowChineseFont(70+32,16*3,16,rtc_clock.week+7); //具体的值 } u8 DS18B20_TEMP_Info[10]; //DS18B20温度信息 /* 函数功能: DS18B20温度显示页面 */ void DS18B20_ShowPageTable(short DS18B20_temp) { char DS18B20_buff[10]; //存放温度信息 unsigned short DS18B20_intT=0,DS18B20_decT=0; //温度值的整数和小数部分 DS18B20_intT = DS18B20_temp >> 4; //分离出温度值整数部分 DS18B20_decT = DS18B20_temp & 0xF; //分离出温度值小数部分 sprintf((char*)DS18B20_TEMP_Info,"%d.%d",DS18B20_intT,DS18B20_decT); //保存DS18B20温度信息,发送给上位机 OLED_ShowString(34,0,16,"DS18B20"); if(DS18B20_temp==0xFF) { OLED_ShowString(0,30,16," "); //清除一行的显示 //显示温度错误信息 OLED_ShowString(0,30,16,"DS18B20 Error!"); } else { sprintf(DS18B20_buff,"%sC ",DS18B20_TEMP_Info); //显示温度 OLED_ShowString(40,30,16,DS18B20_buff); } } int main(void) { u8 stat; u8 key_val; u32 TimeCnt=0; u16 temp_data; //温度数据 short aacx,aacy,aacz; //加速度传感器原始数据 short gyrox,gyroy,gyroz; //陀螺仪原始数据 short temp; float pitch,roll,yaw; //欧拉角 u8 page_cnt=0; //显示的页面 u8 display_state1=0; u8 display_state2=0; BEEP_Init(); //初始化蜂鸣器 LED_Init(); //初始化LED KEY_Init(); //按键初始化 DS18B20_Init(); //DS18B20 USARTx_Init(USART1,72,115200);//串口1的初始化 TIMERx_Init(TIM1,72,20000); //辅助串口1接收。20ms为一帧数据。 RTC_Init(); //RTC初始化 OLED_Init(0xc8,0xa1); //OLED显示屏初始化--正常显示 //OLED_Init(0xc0,0xa0); //OLED显示屏初始化--翻转显示 while(MPU6050_Init()) //初始化MPU6050 { printf("MPU6050陀螺仪初始化失败!\r\n"); DelayMs(500); } // //注意:陀螺仪初始化的时候,必须正常摆放才可以初始化成 // while(mpu_dmp_init()) // { // printf("MPU6050陀螺仪设置DMP失败!\r\n"); // DelayMs(1000); // } OLED_Clear(0x00); //清屏 DrawTimeFrame(); //画时钟框架 while(1) { key_val=KEY_GetValue(); if(key_val) { page_cnt=!page_cnt; //时钟页面 if(page_cnt==0) { //清屏 OLED_Clear(0); DrawTimeFrame(); //画时钟框架 RTC->CRH|=10; //开启秒中断 } else if(page_cnt==1) { //清屏 OLED_Clear(0); RTC->CRH&=~(10); //关闭秒中断 } } if(USART1_RX_STATE) { //*20200530154322 //通过串口1校准RTC时间 if(USART1_RX_BUFF[0]=='*') { rtc_clock.year=(USART1_RX_BUFF[1]-48)*1000+(USART1_RX_BUFF[2]-48)*100+(USART1_RX_BUFF[3]-48)*10+(USART1_RX_BUFF[4]-48)*1; rtc_clock.mon=(USART1_RX_BUFF[5]-48)*10+(USART1_RX_BUFF[6]-48)*1; rtc_clock.day=(USART1_RX_BUFF[7]-48)*10+(USART1_RX_BUFF[8]-48)*1; rtc_clock.hour=(USART1_RX_BUFF[9]-48)*10+(USART1_RX_BUFF[10]-48)*1; rtc_clock.min=(USART1_RX_BUFF[11]-48)*10+(USART1_RX_BUFF[12]-48)*1; rtc_clock.sec=(USART1_RX_BUFF[13]-48)*10+(USART1_RX_BUFF[14]-48)*1; RTC_SetTime(rtc_clock.year,rtc_clock.mon,rtc_clock.day,rtc_clock.hour,rtc_clock.min,rtc_clock.sec); OLED_Clear(0); //OLED清屏 DrawTimeFrame();//画时钟框架 } USART1_RX_STATE=0; USART1_RX_CNT=0; } //时间记录 DelayMs(10); TimeCnt++; if(TimeCnt>=100) //1000毫秒一次 { TimeCnt=0; LED1=!LED1; temp_data=DS18B20_ReadTemp(); // printf("temp_data=%d.%d\n",temp_data>>4,temp_data&0xF); // stat=mpu_dmp_get_data(&pitch,&roll,&yaw); // temp=MPU6050_Get_Temperature(); //得到温度值 //MPU6050_Get_Gyroscope(&gyrox,&gyroy,&gyroz); //得到陀螺仪原始数据 MPU6050_Get_Accelerometer(&aacx,&aacy,&aacz); //得到加速度传感器数据 //printf("温度数据:%d\r\n",temp); // printf("陀螺仪原始数据 :x=%d y=%d z=%d\r\n",gyrox,gyroy,gyroz); printf("加速度传感器数据:x=%d y=%d z=%d\r\n",aacx,aacy,aacz); // printf("欧垃角:横滚角=%d 俯仰角=%d 航向角=%d\r\n",(int)(roll*100),(int)(pitch*100),(int)(yaw*10)); // //正着显示 if(aacz>=15000) { printf("正着显示\n"); if(display_state1!=1) { display_state2=0; display_state1=1; OLED_Init(0xc8,0xa1); //OLED显示屏初始化--正常显示 } } //翻转显示 else if(display_state2!=1) { printf("反着显示\n"); display_state1=0; display_state2=1; OLED_Init(0xc0,0xa0); //OLED显示屏初始化--翻转显示 } } if(page_cnt==1) //温度显示页面 { DS18B20_ShowPageTable(temp_data); } } } ``` ## 3.2 mpu6050.c ```cpp #include "mpu6050.h" #include "sys.h" #include "delay.h" #include /*--------------------------------------------------------------------IIC协议底层模拟时序--------------------------------------------------------------------------------*/ /* 硬件接线: 1 VCC 3.3V/5V 电源输入 --->接3.3V 2 GND 地线 --->接GND 3 IIC_SDA IIC 通信数据线 -->PB6 4 IIC_SCL IIC 通信时钟线 -->PB7 5 MPU_INT 中断输出引脚 ---->未接 6 MPU_AD0 IIC 从机地址设置引脚-->未接 AD0引脚说明:ID=0X68(悬空/接 GND) ID=0X69(接 VCC) */ /* 函数功能:MPU IIC 延时函数 */ void MPU6050_IIC_Delay(void) { DelayUs(2); } /* 函数功能: 初始化IIC */ void MPU6050_IIC_Init(void) { RCC->APB2ENR|=13; //先使能外设IO PORTB时钟 GPIOB->CRL&=0X00FFFFFF; //PB6/7 推挽输出 GPIOB->CRL|=0X33000000; GPIOB->ODR|=36; //PB6,7 输出高 } /* 函数功能: 产生IIC起始信号 */ void MPU6050_IIC_Start(void) { MPU6050_SDA_OUT(); //sda线输出 MPU6050_IIC_SDA=1; MPU6050_IIC_SCL=1; MPU6050_IIC_Delay(); MPU6050_IIC_SDA=0;//START:when CLK is high,DATA change form high to low MPU6050_IIC_Delay(); MPU6050_IIC_SCL=0;//钳住I2C总线,准备发送或接收数据 } /* 函数功能: 产生IIC停止信号 */ void MPU6050_IIC_Stop(void) { MPU6050_SDA_OUT();//sda线输出 MPU6050_IIC_SCL=0; MPU6050_IIC_SDA=0;//STOP:when CLK is high DATA change form low to high MPU6050_IIC_Delay(); MPU6050_IIC_SCL=1; MPU6050_IIC_SDA=1;//发送I2C总线结束信号 MPU6050_IIC_Delay(); } /* 函数功能: 等待应答信号到来 返 回 值:1,接收应答失败 0,接收应答成功 */ u8 MPU6050_IIC_Wait_Ack(void) { u8 ucErrTime=0; MPU6050_SDA_IN(); //SDA设置为输入 MPU6050_IIC_SDA=1;MPU6050_IIC_Delay(); MPU6050_IIC_SCL=1;MPU6050_IIC_Delay(); while(MPU6050_READ_SDA) { ucErrTime++; if(ucErrTime>250) { MPU6050_IIC_Stop(); return 1; } } MPU6050_IIC_SCL=0;//时钟输出0 return 0; } /* 函数功能:产生ACK应答 */ void MPU6050_IIC_Ack(void) { MPU6050_IIC_SCL=0; MPU6050_SDA_OUT(); MPU6050_IIC_SDA=0; MPU6050_IIC_Delay(); MPU6050_IIC_SCL=1; MPU6050_IIC_Delay(); MPU6050_IIC_SCL=0; } /* 函数功能:不产生ACK应答 */ void MPU6050_IIC_NAck(void) { MPU6050_IIC_SCL=0; MPU6050_SDA_OUT(); MPU6050_IIC_SDA=1; MPU6050_IIC_Delay(); MPU6050_IIC_SCL=1; MPU6050_IIC_Delay(); MPU6050_IIC_SCL=0; } /* 函数功能:IIC发送一个字节 返回从机有无应答 1,有应答 0,无应答 */ void MPU6050_IIC_Send_Byte(u8 txd) { u8 t; MPU6050_SDA_OUT(); MPU6050_IIC_SCL=0;//拉低时钟开始数据传输 for(t=0;t8;t++) { MPU6050_IIC_SDA=(txd&0x80)>>7; txd=1; MPU6050_IIC_SCL=1; MPU6050_IIC_Delay(); MPU6050_IIC_SCL=0; MPU6050_IIC_Delay(); } } /* 函数功能:读1个字节,ack=1时,发送ACK,ack=0,发送nACK */ u8 MPU6050_IIC_Read_Byte(unsigned char ack) { unsigned char i,receive=0; MPU6050_SDA_IN();//SDA设置为输入 for(i=0;i8;i++ ) { MPU6050_IIC_SCL=0; MPU6050_IIC_Delay(); MPU6050_IIC_SCL=1; receive=1; if(MPU6050_READ_SDA)receive++; MPU6050_IIC_Delay(); } if(!ack) MPU6050_IIC_NAck();//发送nACK else MPU6050_IIC_Ack(); //发送ACK return receive; } /*--------------------------------------------------------------------MPU6050底层驱动代码--------------------------------------------------------------------------------*/ /* 函数功能:初始化MPU6050 返 回 值:0,成功 其他,错误代码 */ u8 MPU6050_Init(void) { u8 res; MPU6050_IIC_Init();//初始化IIC总线 MPU6050_Write_Byte(MPU_PWR_MGMT1_REG,0X80); //复位MPU6050 DelayMs(100); MPU6050_Write_Byte(MPU_PWR_MGMT1_REG,0X00); //唤醒MPU6050 MPU6050_Set_Gyro_Fsr(3); //陀螺仪传感器,±2000dps MPU6050_Set_Accel_Fsr(0); //加速度传感器,±2g MPU6050_Set_Rate(50); //设置采样率50Hz MPU6050_Write_Byte(MPU_INT_EN_REG,0X00); //关闭所有中断 MPU6050_Write_Byte(MPU_USER_CTRL_REG,0X00); //I2C主模式关闭 MPU6050_Write_Byte(MPU_FIFO_EN_REG,0X00); //关闭FIFO MPU6050_Write_Byte(MPU_INTBP_CFG_REG,0X80); //INT引脚低电平有效 res=MPU6050_Read_Byte(MPU_DEVICE_ID_REG); if(res==MPU6050_ADDR)//器件ID正确 { MPU6050_Write_Byte(MPU_PWR_MGMT1_REG,0X01); //设置CLKSEL,PLL X轴为参考 MPU6050_Write_Byte(MPU_PWR_MGMT2_REG,0X00); //加速度与陀螺仪都工作 MPU6050_Set_Rate(50); //设置采样率为50Hz }else return 1; return 0; } /* 设置MPU6050陀螺仪传感器满量程范围 fsr:0,±250dps;1,±500dps;2,±1000dps;3,±2000dps 返回值:0,设置成功 其他,设置失败 */ u8 MPU6050_Set_Gyro_Fsr(u8 fsr) { return MPU6050_Write_Byte(MPU_GYRO_CFG_REG,fsr3);//设置陀螺仪满量程范围 } /* 函数功能:设置MPU6050加速度传感器满量程范围 函数功能:fsr:0,±2g;1,±4g;2,±8g;3,±16g 返 回 值:0,设置成功 其他,设置失败 */ u8 MPU6050_Set_Accel_Fsr(u8 fsr) { return MPU6050_Write_Byte(MPU_ACCEL_CFG_REG,fsr3);//设置加速度传感器满量程范围 } /* 函数功能:设置MPU6050的数字低通滤波器 函数参数:lpf:数字低通滤波频率(Hz) 返 回 值:0,设置成功 其他,设置失败 */ u8 MPU6050_Set_LPF(u16 lpf) { u8 data=0; if(lpf>=188)data=1; else if(lpf>=98)data=2; else if(lpf>=42)data=3; else if(lpf>=20)data=4; else if(lpf>=10)data=5; else data=6; return MPU6050_Write_Byte(MPU_CFG_REG,data);//设置数字低通滤波器 } /* 函数功能:设置MPU6050的采样率(假定Fs=1KHz) 函数参数:rate:4~1000(Hz) 返 回 值:0,设置成功 其他,设置失败 */ u8 MPU6050_Set_Rate(u16 rate) { u8 data; if(rate>1000)rate=1000; if(rate4)rate=4; data=1000/rate-1; data=MPU6050_Write_Byte(MPU_SAMPLE_RATE_REG,data); //设置数字低通滤波器 return MPU6050_Set_LPF(rate/2); //自动设置LPF为采样率的一半 } /* 函数功能:得到温度值 返 回 值:返回值:温度值(扩大了100倍) */ short MPU6050_Get_Temperature(void) { u8 buf[2]; short raw; float temp; MPU6050_Read_Len(MPU6050_ADDR,MPU_TEMP_OUTH_REG,2,buf); raw=((u16)buf[0]8)|buf[1]; temp=36.53+((double)raw)/340; return temp*100;; } /* 函数功能:得到陀螺仪值(原始值) 函数参数:gx,gy,gz:陀螺仪x,y,z轴的原始读数(带符号) 返 回 值:0,成功,其他,错误代码 */ u8 MPU6050_Get_Gyroscope(short *gx,short *gy,short *gz) { u8 buf[6],res; res=MPU6050_Read_Len(MPU6050_ADDR,MPU_GYRO_XOUTH_REG,6,buf); if(res==0) { *gx=((u16)buf[0]8)|buf[1]; *gy=((u16)buf[2]8)|buf[3]; *gz=((u16)buf[4]8)|buf[5]; } return res;; } /* 函数功能:得到加速度值(原始值) 函数参数:gx,gy,gz:陀螺仪x,y,z轴的原始读数(带符号) 返 回 值:0,成功,其他,错误代码 */ u8 MPU6050_Get_Accelerometer(short *ax,short *ay,short *az) { u8 buf[6],res; res=MPU6050_Read_Len(MPU6050_ADDR,MPU_ACCEL_XOUTH_REG,6,buf); if(res==0) { *ax=((u16)buf[0]8)|buf[1]; *ay=((u16)buf[2]8)|buf[3]; *az=((u16)buf[4]8)|buf[5]; } return res;; } /* 函数功能:IIC连续写 函数参数: addr:器件地址 reg:寄存器地址 len:写入长度 buf:数据区 返 回 值:0,成功,其他,错误代码 */ u8 MPU6050_Write_Len(u8 addr,u8 reg,u8 len,u8 *buf) { u8 i; MPU6050_IIC_Start(); MPU6050_IIC_Send_Byte((addr1)|0);//发送器件地址+写命令 if(MPU6050_IIC_Wait_Ack()) //等待应答 { MPU6050_IIC_Stop(); return 1; } MPU6050_IIC_Send_Byte(reg); //写寄存器地址 MPU6050_IIC_Wait_Ack(); //等待应答 for(i=0;i/发送数据 if(MPU6050_IIC_Wait_Ack()) //等待ACK { MPU6050_IIC_Stop(); return 1; } } MPU6050_IIC_Stop(); return 0; } /* 函数功能:IIC连续写 函数参数: IIC连续读 addr:器件地址 reg:要读取的寄存器地址 len:要读取的长度 buf:读取到的数据存储区 返 回 值:0,成功,其他,错误代码 */ u8 MPU6050_Read_Len(u8 addr,u8 reg,u8 len,u8 *buf) { MPU6050_IIC_Start(); MPU6050_IIC_Send_Byte((addr1)|0);//发送器件地址+写命令 if(MPU6050_IIC_Wait_Ack()) //等待应答 { MPU6050_IIC_Stop(); return 1; } MPU6050_IIC_Send_Byte(reg); //写寄存器地址 MPU6050_IIC_Wait_Ack(); //等待应答 MPU6050_IIC_Start(); MPU6050_IIC_Send_Byte((addr1)|1);//发送器件地址+读命令 MPU6050_IIC_Wait_Ack(); //等待应答 while(len) { if(len==1)*buf=MPU6050_IIC_Read_Byte(0);//读数据,发送nACK else *buf=MPU6050_IIC_Read_Byte(1); //读数据,发送ACK len--; buf++; } MPU6050_IIC_Stop(); //产生一个停止条件 return 0; } /* 函数功能:IIC写一个字节 函数参数: reg:寄存器地址 data:数据 返 回 值:0,成功,其他,错误代码 */ u8 MPU6050_Write_Byte(u8 reg,u8 data) { MPU6050_IIC_Start(); MPU6050_IIC_Send_Byte((MPU6050_ADDR1)|0);//发送器件地址+写命令 if(MPU6050_IIC_Wait_Ack()) //等待应答 { MPU6050_IIC_Stop(); return 1; } MPU6050_IIC_Send_Byte(reg); //写寄存器地址 MPU6050_IIC_Wait_Ack(); //等待应答 MPU6050_IIC_Send_Byte(data);//发送数据 if(MPU6050_IIC_Wait_Ack()) //等待ACK { MPU6050_IIC_Stop(); return 1; } MPU6050_IIC_Stop(); return 0; } /* 函数功能:IIC读一个字节 函数参数: reg:寄存器地址 data:数据 返 回 值:返回值:读到的数据 */ u8 MPU6050_Read_Byte(u8 reg) { u8 res; MPU6050_IIC_Start(); MPU6050_IIC_Send_Byte((MPU6050_ADDR1)|0);//发送器件地址+写命令 MPU6050_IIC_Wait_Ack(); //等待应答 MPU6050_IIC_Send_Byte(reg); //写寄存器地址 MPU6050_IIC_Wait_Ack(); //等待应答 MPU6050_IIC_Start(); MPU6050_IIC_Send_Byte((MPU6050_ADDR1)|1);//发送器件地址+读命令 MPU6050_IIC_Wait_Ack(); //等待应答 res=MPU6050_IIC_Read_Byte(0);//读取数据,发送nACK MPU6050_IIC_Stop(); //产生一个停止条件 return res; } ``` ## 3.3 mpu6050.h ```cpp #ifndef __MPU6050_H #define __MPU6050_H #include "stm32f10x.h" #define MPU_SELF_TESTX_REG 0X0D //自检寄存器X #define MPU_SELF_TESTY_REG 0X0E //自检寄存器Y #define MPU_SELF_TESTZ_REG 0X0F //自检寄存器Z #define MPU_SELF_TESTA_REG 0X10 //自检寄存器A #define MPU_SAMPLE_RATE_REG 0X19 //采样频率分频器 #define MPU_CFG_REG 0X1A //配置寄存器 #define MPU_GYRO_CFG_REG 0X1B //陀螺仪配置寄存器 #define MPU_ACCEL_CFG_REG 0X1C //加速度计配置寄存器 #define MPU_MOTION_DET_REG 0X1F //运动检测阀值设置寄存器 #define MPU_FIFO_EN_REG 0X23 //FIFO使能寄存器 #define MPU_I2CMST_CTRL_REG 0X24 //IIC主机控制寄存器 #define MPU_I2CSLV0_ADDR_REG 0X25 //IIC从机0器件地址寄存器 #define MPU_I2CSLV0_REG 0X26 //IIC从机0数据地址寄存器 #define MPU_I2CSLV0_CTRL_REG 0X27 //IIC从机0控制寄存器 #define MPU_I2CSLV1_ADDR_REG 0X28 //IIC从机1器件地址寄存器 #define MPU_I2CSLV1_REG 0X29 //IIC从机1数据地址寄存器 #define MPU_I2CSLV1_CTRL_REG 0X2A //IIC从机1控制寄存器 #define MPU_I2CSLV2_ADDR_REG 0X2B //IIC从机2器件地址寄存器 #define MPU_I2CSLV2_REG 0X2C //IIC从机2数据地址寄存器 #define MPU_I2CSLV2_CTRL_REG 0X2D //IIC从机2控制寄存器 #define MPU_I2CSLV3_ADDR_REG 0X2E //IIC从机3器件地址寄存器 #define MPU_I2CSLV3_REG 0X2F //IIC从机3数据地址寄存器 #define MPU_I2CSLV3_CTRL_REG 0X30 //IIC从机3控制寄存器 #define MPU_I2CSLV4_ADDR_REG 0X31 //IIC从机4器件地址寄存器 #define MPU_I2CSLV4_REG 0X32 //IIC从机4数据地址寄存器 #define MPU_I2CSLV4_DO_REG 0X33 //IIC从机4写数据寄存器 #define MPU_I2CSLV4_CTRL_REG 0X34 //IIC从机4控制寄存器 #define MPU_I2CSLV4_DI_REG 0X35 //IIC从机4读数据寄存器 #define MPU_I2CMST_STA_REG 0X36 //IIC主机状态寄存器 #define MPU_INTBP_CFG_REG 0X37 //中断/旁路设置寄存器 #define MPU_INT_EN_REG 0X38 //中断使能寄存器 #define MPU_INT_STA_REG 0X3A //中断状态寄存器 #define MPU_ACCEL_XOUTH_REG 0X3B //加速度值,X轴高8位寄存器 #define MPU_ACCEL_XOUTL_REG 0X3C //加速度值,X轴低8位寄存器 #define MPU_ACCEL_YOUTH_REG 0X3D //加速度值,Y轴高8位寄存器 #define MPU_ACCEL_YOUTL_REG 0X3E //加速度值,Y轴低8位寄存器 #define MPU_ACCEL_ZOUTH_REG 0X3F //加速度值,Z轴高8位寄存器 #define MPU_ACCEL_ZOUTL_REG 0X40 //加速度值,Z轴低8位寄存器 #define MPU_TEMP_OUTH_REG 0X41 //温度值高八位寄存器 #define MPU_TEMP_OUTL_REG 0X42 //温度值低8位寄存器 #define MPU_GYRO_XOUTH_REG 0X43 //陀螺仪值,X轴高8位寄存器 #define MPU_GYRO_XOUTL_REG 0X44 //陀螺仪值,X轴低8位寄存器 #define MPU_GYRO_YOUTH_REG 0X45 //陀螺仪值,Y轴高8位寄存器 #define MPU_GYRO_YOUTL_REG 0X46 //陀螺仪值,Y轴低8位寄存器 #define MPU_GYRO_ZOUTH_REG 0X47 //陀螺仪值,Z轴高8位寄存器 #define MPU_GYRO_ZOUTL_REG 0X48 //陀螺仪值,Z轴低8位寄存器 #define MPU_I2CSLV0_DO_REG 0X63 //IIC从机0数据寄存器 #define MPU_I2CSLV1_DO_REG 0X64 //IIC从机1数据寄存器 #define MPU_I2CSLV2_DO_REG 0X65 //IIC从机2数据寄存器 #define MPU_I2CSLV3_DO_REG 0X66 //IIC从机3数据寄存器 #define MPU_I2CMST_DELAY_REG 0X67 //IIC主机延时管理寄存器 #define MPU_SIGPATH_RST_REG 0X68 //信号通道复位寄存器 #define MPU_MDETECT_CTRL_REG 0X69 //运动检测控制寄存器 #define MPU_USER_CTRL_REG 0X6A //用户控制寄存器 #define MPU_PWR_MGMT1_REG 0X6B //电源管理寄存器1 #define MPU_PWR_MGMT2_REG 0X6C //电源管理寄存器2 #define MPU_FIFO_CNTH_REG 0X72 //FIFO计数寄存器高八位 #define MPU_FIFO_CNTL_REG 0X73 //FIFO计数寄存器低八位 #define MPU_FIFO_RW_REG 0X74 //FIFO读写寄存器 #define MPU_DEVICE_ID_REG 0X75 //器件ID寄存器 //重力加速度值,单位:9.5 m/s2 typedef struct { float accX; float accY; float accZ; }accValue_t; //因为模块AD0默认接GND,所以转为读写地址后,为0XD1和0XD0(如果接VCC,则为0XD3和0XD2) 从AD0接地机地址为:0X68 u8 MPU6050_Init(void); //初始化MPU6050 u8 MPU6050_Write_Len(u8 addr,u8 reg,u8 len,u8 *buf);//IIC连续写 u8 MPU6050_Read_Len(u8 addr,u8 reg,u8 len,u8 *buf); //IIC连续读 u8 MPU6050_Write_Byte(u8 reg,u8 data); //IIC写一个字节 u8 MPU6050_Read_Byte(u8 reg); //IIC读一个字节 u8 MPU6050_Set_Gyro_Fsr(u8 fsr); u8 MPU6050_Set_Accel_Fsr(u8 fsr); u8 MPU6050_Set_LPF(u16 lpf); u8 MPU6050_Set_Rate(u16 rate); u8 MPU6050_Set_Fifo(u8 sens); short MPU6050_Get_Temperature(void); u8 MPU6050_Get_Gyroscope(short *gx,short *gy,short *gz); u8 MPU6050_Get_Accelerometer(short *ax,short *ay,short *az); //如果AD0脚(9脚)接地,IIC地址为0X68(不包含最低位). //如果接V3.3,则IIC地址为0X69(不包含最低位). #define MPU6050_ADDR 0X68 //IO方向设置 #define MPU6050_SDA_IN() {GPIOB->CRL&=0XF0FFFFFF;GPIOB->CRL|=824;} #define MPU6050_SDA_OUT() {GPIOB->CRL&=0XF0FFFFFF;GPIOB->CRL|=324;} //IO操作函数 #define MPU6050_IIC_SCL PBout(7) //SCL #define MPU6050_IIC_SDA PBout(6) //SDA #define MPU6050_READ_SDA PBin(6) //输入SDA #endif ```
  • [技术干货] 基于STM32单片机设计的红外测温仪(带人脸检测)
    # 基于STM32单片机设计的红外测温仪(带人脸检测) 由于医学发展的需要,在很多情况下,一般的温度计己经满足不了快速而又准确的测温要求,例如:车站、地铁、机场等人口密度较大的地方进行人体温度测量。 当前设计的这款红外测温仪由测温硬件+上位机软件组合而成,主要用在地铁、车站入口等地方,可以准确识别人脸进行测温,如果有人温度超标会进行语音提示并且保存当前人脸照片。 ## 1、 硬件选型与设计思路 ### (1). 设备端 主控单片机采用STM32F103C8T6,人体测温功能采用非接触式红外测温模块。 ![image.png](https://bbs-img.huaweicloud.com/data/forums/attachment/forum/20227/6/1657088443080712875.png) ### (2). 上位机设计思路 上位机采用Qt5设计,Qt5是一套基于C++语言的跨平台软件库,性能非常强大,目前桌面端很多主流的软件都是采用QT开发。比如: 金山办公旗下的-WPS,字节跳动旗下的-剪映,暴雪娱乐公司旗下-多款游戏登录器等等。Qt在车联网领域用的也非常多,比如,哈佛,特斯拉,比亚迪等等很多车的中控屏整个系统都是采用Qt设计。 在测温项目里,上位机与STM32之间采用串口协议进行通信,上位机可以打开笔记本电脑默认的摄像头,进行人脸检测;当检测到人脸时,控制STM32测量当前人体的实时温度实时,再将温度传递到上位机显示;当温度正常时,上位机上显示绿色的提示字样“温度正常”,并有语音播报,语音播报的声音使用笔记本自带的声卡发出。如果温度过高,上位机显示红色提示字样“温度异常,请重新测量”,并有语音播报提示。温度过高时,会自动将当前人脸拍照留存,照片存放在当前软件目录下的“face”目录里,文件的命名规则是“38.8_2022-01-05-22-12-34.jpg”,其中38.8表示温度值,后面是日期(年月日时分秒)。 ### (3) 上位机运行效果 ![image.png](https://bbs-img.huaweicloud.com/data/forums/attachment/forum/20227/6/1657088458540921923.png) ![image.png](https://bbs-img.huaweicloud.com/data/forums/attachment/forum/20227/6/1657088467253561881.png) 上位机需要连接STM32设备之后才可以获取温度数据,点击软件上的打开摄像头按钮,开启摄像头,让检测到人脸时,下面会显示当前测量的温度。如果没有连接STM32设备,那么默认会显示一个正常的固定温度值。界面上右边红色的字,表示当前处理一帧图像的耗时时间,电脑性能越好,检测速度越快。 ### (4) 拿到可执行文件之后如何运行? 先解压压缩包,进入“测温仪上位机-可执行文件”目录,将“haarcascade_frontalface_alt2.xml”拷贝到C盘根目录。 ![img](https://bbs-img.huaweicloud.com/blogs/img/16e6b1.png) ![img](https://bbs-img.huaweicloud.com/blogs/img/17a28c.png) 然后双击“FaceTemperatureCheck.exe”运行程序。 ![img](https://bbs-img.huaweicloud.com/blogs/img/18c1fe.png) 未连接设备,也可以打开摄像头检测人脸,只不过温度值是一个固定的正常温度值范围。 ## 二、上位机设计 ## 2.1 安装编译环境 如果需要自己编译运行源代码,需要先安装Qt5开发环境。 下载地址: https://download.qt.io/official_releases/qt/5.12/5.12.0/ ![img](https://bbs-img.huaweicloud.com/blogs/img/197e29.png) 下载之后,先将电脑网络断掉(不然会提示要求输入QT的账号),然后双击安装包进行安装。 安装可以只需要选择一个MinGW 32位编译器就够用了,详情看下面截图,选择“MinGW 7.3.0 32-bit”后,就点击下一步,然后等待安装完成即可。 ![img](https://bbs-img.huaweicloud.com/blogs/img/1b27e0.png) ## 2.2 软件代码整体效果 如果需要完整的工程,可以在这里去下载:https://download.csdn.net/download/xiaolong1126626497/85892490 ![img](https://bbs-img.huaweicloud.com/blogs/img/1d3671.png) 打开工程后(工程文件的后缀是xxx.pro),点击左下角的绿色三角形按钮就可以编译运行程序。 ![img](https://bbs-img.huaweicloud.com/blogs/img/206514.png) ## 2.3 UI设计界面 ![img](https://bbs-img.huaweicloud.com/blogs/img/239444.png) ## 2.4 人脸检测核心代码 ```cpp //人脸检测代码 bool ImageHandle::opencv_face(QImage qImage) { bool check_flag=0; QTime time; time.start(); static CvMemStorage* storage = nullptr; static CvHaarClassifierCascade* cascade = nullptr; //模型文件路径 QString face_model_file = QCoreApplication::applicationDirPath()+"/"+FACE_MODEL_FILE; //加载分类器:正面脸检测 cascade = (CvHaarClassifierCascade*)cvLoad(face_model_file.toUtf8().data(), 0, 0, 0 ); if(!cascade) { qDebug()"分类器加载错误.\n"; return check_flag; } //创建内存空间 storage = cvCreateMemStorage(0); //加载需要检测的图片 IplImage* img = QImageToIplImage(&qImage); if(img ==nullptr ) { qDebug()"图片加载错误.\n"; return check_flag; } double scale=1.2; //创建图像首地址,并分配存储空间 IplImage* gray = cvCreateImage(cvSize(img->width,img->height),8,1); //创建图像首地址,并分配存储空间 IplImage* small_img=cvCreateImage(cvSize(cvRound(img->width/scale),cvRound(img->height/scale)),8,1); cvCvtColor(img,gray, CV_BGR2GRAY); cvResize(gray, small_img, CV_INTER_LINEAR); cvEqualizeHist(small_img,small_img); //直方图均衡 /* * 指定相应的人脸特征检测分类器,就可以检测出图片中所有的人脸,并将检测到的人脸通过矩形的方式返回。 * 总共有8个参数,函数说明: 参数1:表示输入图像,尽量使用灰度图以加快检测速度。 参数2:表示Haar特征分类器,可以用cvLoad()函数来从磁盘中加载xml文件作为Haar特征分类器。 参数3:用来存储检测到的候选目标的内存缓存区域。 参数4:表示在前后两次相继的扫描中,搜索窗口的比例系数。默认为1.1即每次搜索窗口依次扩大10% 参数5:表示构成检测目标的相邻矩形的最小个数(默认为3个)。如果组成检测目标的小矩形的个数和小于 min_neighbors - 1 都会被排除。如果min_neighbors 为 0, 则函数不做任何操作就返回所有的被检候选矩形框,这种设定值一般用在用户自定义对检测结果的组合程序上。 参数6:要么使用默认值,要么使用CV_HAAR_DO_CANNY_PRUNING,如果设置为CV_HAAR_DO_CANNY_PRUNING,那么函数将会使用Canny边缘检测来排除边缘过多或过少的区域,因此这些区域通常不会是人脸所在区域。 参数7:表示检测窗口的最小值,一般设置为默认即可。 参数8:表示检测窗口的最大值,一般设置为默认即可。 函数返回值:函数将返回CvSeq对象,该对象包含一系列CvRect表示检测到的人脸矩形。 */ CvSeq* objects = cvHaarDetectObjects(small_img, cascade, storage, 1.1, 3, 0/*CV_HAAR_DO_CANNY_PRUNING*/, cvSize(50,50)/*大小决定了检测时消耗的时间多少*/); qDebug()"人脸数量:"total; //遍历找到对象和周围画盒 QPainter painter(&qImage);//构建 QPainter 绘图对象 QPen pen; pen.setColor(Qt::blue); //画笔颜色 pen.setWidth(5); //画笔宽度 painter.setPen(pen); //设置画笔 CvRect *max=nullptr; for(int i=0;i(objects->total);++i) { //得到人脸的坐标位置和宽度高度信息 CvRect* r=(CvRect*)cvGetSeqElem(objects,i); if(max==nullptr)max=r; else { if(r->width > max->width || r->height > max->height) { max=r; } } } //如果找到最大的目标脸 if(max!=nullptr) { check_flag=true; //将人脸区域绘制矩形圈起来 painter.drawRect(max->x*scale,max->y*scale,max->width*scale,max->height*scale); } cvReleaseImage(&gray); //释放图片内存 cvReleaseImage(&small_img); //释放图片内存 cvReleaseHaarClassifierCascade(&cascade); //释放内存-->分类器 cvReleaseMemStorage(&objects->storage); //释放内存-->检测出图片中所有的人脸 //释放图片 cvReleaseImage(&img); qint32 time_ms=0; time_ms=time.elapsed(); //耗时时间 emit ss_log_text(QString("%1").arg(time_ms)); //保存结果 m_image=qImage.copy(); return check_flag; } ``` ## 2.5 配置文件(修改参数-很重要) ![img](https://bbs-img.huaweicloud.com/blogs/img/2fe364.png) 参数说明: 如果电脑上有多个摄像头,可以修改配置文件里的摄像头编号,具体的数量在程序启动时会自动查询,通过打印代码输出到终端。 如果自己第一次编译运行源码,运行之后, (1)需要将软件源码目录下的“haarcascade_frontalface_alt2.xml” 文件拷贝到C盘根目录,或者其他非中文目录下,具体路径可以在配置文件里修改,默认就是C盘根目录。 (2)需要将软件源码目录下的“OpenCV-MinGW-Build-OpenCV-3.4.7\x86\mingw\bin”目录里所有文件拷贝到,生成的程序执行文件同级目录下。 这样才能保证程序可以正常运行。 报警温度的阀值范围,也可以自行更改,在配置文件里有说明。 ## 2.6 语音提示文件与背景图 语音提示文件,背景图是通过资源文件加载的。 源文件存放在,源代码的“FaceTemperatureCheck\res”目录下。 ![img](https://bbs-img.huaweicloud.com/blogs/img/32b8d5.png) 自己也可以自行替换,重新编译程序即可生效。 ## 2.7 语音播报与图像显示处理代码 ```cpp //图像处理的结果 void Widget::slot_HandleImage(bool flag,QImage image) { bool temp_state=0; //检测到人脸 if(flag) { //判断温度是否正常 if(current_tempMIN_TEMP) { temp_state=true; //显示温度正常 ui->label_temp->setStyleSheet("color: rgb(0, 255, 127);font: 20pt \"Arial\";"); ui->label_temp->setText(QString("%1℃").arg(current_temp)); } else //语音播报,温度异常 { temp_state=false; //显示温度异常 ui->label_temp->setStyleSheet("color: rgb(255, 0, 0);font: 20pt \"Arial\";"); ui->label_temp->setText(QString("%1℃").arg(current_temp)); } //获取当前ms时间 long long currentTime = QDateTime::currentDateTime().toMSecsSinceEpoch(); //判断时间间隔是否到达 if(currentTime-old_currentTime>AUDIO_RATE_MS) { //更改当前时间 old_currentTime=currentTime; //温度正常 if(temp_state) { //语音播报,温度正常 QSound::play(":/res/ok.wav"); } //温度异常 else { //语音播报,温度异常 QSound::play(":/res/error.wav"); //拍照留存 QString dir_str = QCoreApplication::applicationDirPath()+"/face"; //检查目录是否存在,若不存在则新建 QDir dir; if (!dir.exists(dir_str)) { bool res = dir.mkpath(dir_str); qDebug() "新建目录状态:" res; } //目录存在就保存图片 QDir dir2; if (dir2.exists(dir_str)) { //拼接名称 QDateTime dateTime(QDateTime::currentDateTime()); //时间效果: 2020-03-05 16:25::04 周一 QString qStr=QString("%1/%2_").arg(dir_str).arg(current_temp); qStr+=dateTime.toString("yyyy-MM-dd-hh-mm-ss-ddd"); image.save(qStr+".jpg"); } } } } else //不显示温度 { ui->label_temp->setStyleSheet("color: rgb(0, 255, 127);font: 20pt \"Arial\";"); ui->label_temp->setText("----"); } //处理图像的结果画面 ui->widget_player->slotGetOneFrame(image); } ``` ## 2.8 STM32的温度接收处理代码 ```cpp //刷新串口端口 void Widget::on_pushButton_flush_uart_clicked() { QList UartInfoList=QSerialPortInfo::availablePorts(); //获取可用串口端口信息 ui->comboBox_ComSelect->clear(); if(UartInfoList.count()>0) { for(int i=0;i/如果当前串口 COM 口忙就返回真,否则返回假 { QString info=UartInfoList.at(i).portName(); info+="(占用)"; ui->comboBox_ComSelect->addItem(info); //添加新的条目选项 } else { ui->comboBox_ComSelect->addItem(UartInfoList.at(i).portName()); //添加新的条目选项 } } } else { ui->comboBox_ComSelect->addItem("无可用COM口"); //添加新的条目选项 } } //连接测温设备 void Widget::on_pushButton_OpenUart_clicked() { if(ui->pushButton_OpenUart->text()=="连接测温设备") //打开串口 { ui->pushButton_OpenUart->setText("断开连接"); /*配置串口的信息*/ UART_Config->setPortName(ui->comboBox_ComSelect->currentText()); //COM的名称 if(!(UART_Config->open(QIODevice::ReadWrite))) //打开的属性权限 { QMessageBox::warning(this, tr("状态提示"), tr("设备连接失败!\n请选择正确的COM口"), QMessageBox::Ok); ui->pushButton_OpenUart->setText("连接测温设备"); return; } } else //关闭串口 { ui->pushButton_OpenUart->setText("连接测温设备"); /*关闭串口-*/ UART_Config->clear(QSerialPort::AllDirections); UART_Config->close(); } } //读信号 void Widget::ReadUasrtData() { /*返回可读的字节数*/ if(UART_Config->bytesAvailable()=0) { return; } /*定义字节数组*/ QByteArray rx_data; /*读取串口缓冲区所有的数据*/ rx_data=UART_Config->readAll(); //转换温度 current_temp=rx_data.toDouble(); } ```
  • [技术干货] STM32+BH1750光敏传感器获取光照强度
    # 一、环境介绍 **MCU:** STM32F103ZET6 **光敏传感器:** BH1750数字传感器(IIC接口) **开发软件:** Keil5 **代码说明:** 使用IIC模拟时序驱动,方便移植到其他平台,采集的光照度比较灵敏. 合成的光照度返回值范围是 0~255。 0表示全黑 255表示很亮。 实测: 手机闪光灯照着的状态返回值是245左右,手捂着的状态返回值是10左右. ![image.png](https://bbs-img.huaweicloud.com/data/forums/attachment/forum/20227/5/1656998290077879409.png) ![image.png](https://bbs-img.huaweicloud.com/data/forums/attachment/forum/20227/5/1656998309372399500.png) # 二、BH1750介绍 ![image.png](https://bbs-img.huaweicloud.com/data/forums/attachment/forum/20227/5/1656998325073451436.png) ![image.png](https://bbs-img.huaweicloud.com/data/forums/attachment/forum/20227/5/1656998334844912042.png) ![image.png](https://bbs-img.huaweicloud.com/data/forums/attachment/forum/20227/5/1656998345832755842.png) # 三、核心代码 **BH1750说明: ADDR引脚接地,地址就是0x46** ## 3.1 iic.c ```cpp #include "iic.h" /* 函数功能:IIC接口初始化 硬件连接: SDA:PB7 SCL:PB6 */ void IIC_Init(void) { RCC->APB2ENR|=13;//PB GPIOB->CRL&=0x00FFFFFF; GPIOB->CRL|=0x33000000; GPIOB->ODR|=0x36; } /* 函数功能:IIC总线起始信号 */ void IIC_Start(void) { IIC_SDA_OUTMODE(); //初始化SDA为输出模式 IIC_SDA_OUT=1; //数据线拉高 IIC_SCL=1; //时钟线拉高 DelayUs(4); //电平保持时间 IIC_SDA_OUT=0; //数据线拉低 DelayUs(4); //电平保持时间 IIC_SCL=0; //时钟线拉低 } /* 函数功能:IIC总线停止信号 */ void IIC_Stop(void) { IIC_SDA_OUTMODE(); //初始化SDA为输出模式 IIC_SDA_OUT=0; //数据线拉低 IIC_SCL=0; //时钟线拉低 DelayUs(4); //电平保持时间 IIC_SCL=1; //时钟线拉高 DelayUs(4); //电平保持时间 IIC_SDA_OUT=1; //数据线拉高 } /* 函数功能:获取应答信号 返 回 值:1表示失败,0表示成功 */ u8 IIC_GetACK(void) { u8 cnt=0; IIC_SDA_INPUTMODE();//初始化SDA为输入模式 IIC_SDA_OUT=1; //数据线上拉 DelayUs(2); //电平保持时间 IIC_SCL=0; //时钟线拉低,告诉从机,主机需要数据 DelayUs(2); //电平保持时间,等待从机发送数据 IIC_SCL=1; //时钟线拉高,告诉从机,主机现在开始读取数据 while(IIC_SDA_IN) //等待从机应答信号 { cnt++; if(cnt>250)return 1; } IIC_SCL=0; //时钟线拉低,告诉从机,主机需要数据 return 0; } /* 函数功能:主机向从机发送应答信号 函数形参:0表示应答,1表示非应答 */ void IIC_SendACK(u8 stat) { IIC_SDA_OUTMODE(); //初始化SDA为输出模式 IIC_SCL=0; //时钟线拉低,告诉从机,主机需要发送数据 if(stat)IIC_SDA_OUT=1; //数据线拉高,发送非应答信号 else IIC_SDA_OUT=0; //数据线拉低,发送应答信号 DelayUs(2); //电平保持时间,等待时钟线稳定 IIC_SCL=1; //时钟线拉高,告诉从机,主机数据发送完毕 DelayUs(2); //电平保持时间,等待从机接收数据 IIC_SCL=0; //时钟线拉低,告诉从机,主机需要数据 } /* 函数功能:IIC发送1个字节数据 函数形参:将要发送的数据 */ void IIC_WriteOneByteData(u8 data) { u8 i; IIC_SDA_OUTMODE(); //初始化SDA为输出模式 IIC_SCL=0; //时钟线拉低,告诉从机,主机需要发送数据 for(i=0;i8;i++) { if(data&0x80)IIC_SDA_OUT=1; //数据线拉高,发送1 else IIC_SDA_OUT=0; //数据线拉低,发送0 IIC_SCL=1; //时钟线拉高,告诉从机,主机数据发送完毕 DelayUs(2); //电平保持时间,等待从机接收数据 IIC_SCL=0; //时钟线拉低,告诉从机,主机需要发送数据 DelayUs(2); //电平保持时间,等待时钟线稳定 data=1; //先发高位 } } /* 函数功能:IIC接收1个字节数据 返 回 值:收到的数据 */ u8 IIC_ReadOneByteData(void) { u8 i,data; IIC_SDA_INPUTMODE();//初始化SDA为输入模式 for(i=0;i8;i++) { IIC_SCL=0; //时钟线拉低,告诉从机,主机需要数据 DelayUs(2); //电平保持时间,等待从机发送数据 IIC_SCL=1; //时钟线拉高,告诉从机,主机现在正在读取数据 data=1; if(IIC_SDA_IN)data|=0x01; DelayUs(2); //电平保持时间,等待时钟线稳定 } IIC_SCL=0; //时钟线拉低,告诉从机,主机需要数据 (必须拉低,否则将会识别为停止信号) return data; } ``` ## 3.2 iic.h ```cpp #ifndef _IIC_H #define _IIC_H #include "stm32f10x.h" #include "sys.h" #include "delay.h" #define IIC_SDA_OUTMODE() {GPIOB->CRL&=0x0FFFFFFF;GPIOB->CRL|=0x30000000;} #define IIC_SDA_INPUTMODE() {GPIOB->CRL&=0x0FFFFFFF;GPIOB->CRL|=0x80000000;} #define IIC_SDA_OUT PBout(7) //数据线输出 #define IIC_SDA_IN PBin(7) //数据线输入 #define IIC_SCL PBout(6) //时钟线 void IIC_Init(void); void IIC_Start(void); void IIC_Stop(void); u8 IIC_GetACK(void); void IIC_SendACK(u8 stat); void IIC_WriteOneByteData(u8 data); u8 IIC_ReadOneByteData(void); #endif ``` ## 3.3 BH1750.h ```cpp #ifndef _BH1750_H #define _BH1750_H #include "delay.h" #include "iic.h" #include "usart.h" u8 Read_BH1750_Data(void); #endif ``` ## 3.4 BH1750.c ```cpp #include "bh1750.h" u8 Read_BH1750_Data() { unsigned char t0; unsigned char t1; unsigned char t; u8 r_s=0; IIC_Start(); //发送起始信号 IIC_WriteOneByteData(0x46); r_s=IIC_GetACK();//获取应答 if(r_s)printf("error:1\r\n"); IIC_WriteOneByteData(0x01); r_s=IIC_GetACK();//获取应答 if(r_s)printf("error:2\r\n"); IIC_Stop(); //停止信号 IIC_Start(); //发送起始信号 IIC_WriteOneByteData(0x46); r_s=IIC_GetACK();//获取应答 if(r_s)printf("error:3\r\n"); IIC_WriteOneByteData(0x01); r_s=IIC_GetACK();//获取应答 if(r_s)printf("error:4\r\n"); IIC_Stop(); //停止信号 IIC_Start(); //发送起始信号 IIC_WriteOneByteData(0x46); r_s=IIC_GetACK();//获取应答 if(r_s)printf("error:5\r\n"); IIC_WriteOneByteData(0x10); r_s=IIC_GetACK();//获取应答 if(r_s)printf("error:6\r\n"); IIC_Stop(); //停止信号 DelayMs(300); //等待 IIC_Start(); //发送起始信号 IIC_WriteOneByteData(0x47); r_s=IIC_GetACK();//获取应答 if(r_s)printf("error:7\r\n"); t0=IIC_ReadOneByteData(); //接收数据 IIC_SendACK(0); //发送应答信号 t1=IIC_ReadOneByteData(); //接收数据 IIC_SendACK(1); //发送非应答信号 IIC_Stop(); //停止信号 t=(((t08)|t1)/1.2); return t; } ``` ## 3.5 main.c ```cpp 如果需要完整工程可以去这里下载:https://download.csdn.net/download/xiaolong1126626497/18500653 #include "stm32f10x.h" #include "led.h" #include "delay.h" #include "key.h" #include "usart.h" #include "at24c02.h" #include "bh1750.h" int main() { u8 val; LED_Init(); BEEP_Init(); KeyInit(); USARTx_Init(USART1,72,115200); IIC_Init(); while(1) { val=KeyScan(); if(val) { val=Read_BH1750_Data(); printf("光照强度=%d\r\n",val); // BEEP=!BEEP; LED0=!LED0; LED1=!LED1; } } } ``` ## 3.6 运行效果图 ![image.png](https://bbs-img.huaweicloud.com/data/forums/attachment/forum/20227/5/1656998363594650425.png)
  • [行业资讯] 运算放大器功耗与性能的权衡
    高性能,低功耗:越来越多的应用需要满足这一需求,尤其是由电池供电的移动设备。特别是在、工业4.0和数字化时代,这些手持设备大大方便了人们的日常生活。从移动生命体征监测到工业环境中的机器和系统监测,很多应用纷纷受益。智能手机和可穿戴设备等终端用户产品也要求更高的性能和更长的电池寿命。因为提供电源的电池电能有限,所以需要在使用消耗电流最小的元件,以最大限度延长设备的运行时间。或者,通过降低功耗,使低容量电池也可以实现相同的电池寿命,同时减小尺寸、重量和成本。温度管理同样不容忽视。同样,更高效的元件也起积极作用。冷却管理需要占用空间,如果产生的热量减少,占用的空间也会减少。目前,市面上提供多种低功耗,甚至是超低功耗(ULP)元件。本文着重探讨低功耗运算放大器。功耗与性能的权衡在选择合适的放大器时,往往需要考虑运算放大器的功耗,并做出权衡。低功耗往往也意味着低带宽。但是,这也取决于给定的放大器架构和稳定性要求。寄生电容和电感越高,通常带宽越低。例如,电流反馈放大器提供相对较高的带宽,但精准度较低。我们可以使用一些技巧来提高带宽-功率比。例如,增益带宽积(GBW)一般如下:                                           Gm表示跨导,或者是输出电流和输入电压之比(IOUT/VIN),C表示内部补偿电容。增加带宽的典型方法是增加偏置电流,这会使Gm增加,但会消耗更多功率。为了保持低功率,我们不愿意如此。通常,补偿电容会设置主极点,所以理想情况下,负载电容根本不会影响带宽。受放大器的物理特性限制,电容较低时,通常可以获得更高带宽,但这也会影响稳定性,在低噪声增益下,其稳定性会得到提高。但是,实际上,我们无法在更低噪声增益下驱动大型的纯电容负载。在使用低功耗运算放大器时,需要权衡的另一因素是通常较高的电压噪声。但是,输入电压噪声是放大器最主要的噪声(占总输出宽带噪声的一部分),但也可能是电阻噪声。总噪声最主要的部分可能来自于输入级中的噪声源(例如,集电极产生散射噪声,漏极产生热噪声)。1/f噪声(闪烁噪声)因架构而异,是由元件材料中的特殊缺陷引起的。所以,它一般取决于元件的大小。相反,电流噪声在更低的功率电平下通常更低。但也不容忽视,尤其是在双极放大器中。在1/f区域,1/f电流噪声是放大器输出端的总1/f噪声的主要来源。其他权衡因素包括失真性能和漂移值。低功耗运算放大器通常表现出更高的总谐波失真(THD),但是和电流噪声一样,双极放大器中的输入偏置和失调电流会随着电源电流降低而降低。失调电压是运算放大器的另一个重要指标。一般可通过调整输入端元件来降低影响,因此不会在低功率下导致性能大幅降低,所以VOS和VOS漂移在功率范围内是恒定的。外部电路和反馈电阻(RF)也会影响运算放大器的性能。电阻值较高时,动态功率和谐波失真会降低,但它们会增大输出噪声,以及与偏置电流相关的误差。为了进一步降低功耗,许多设备都提供待机或睡眠功能。这样重要设备功能在闲置时可以停用,根据需要重新激活。低功耗放大器的唤醒时间通常更长。表1对前文所述的权衡因素进行了归纳和汇总。表1.低功耗运算放大器的权衡功耗反馈电阻(RF) á正作用电流噪声  偏置电流漂移  失调电流漂移?动态功率失真(THD)   @ HF 负作用带宽  电压噪声  失真(THD)   @ HF  唤醒时间  驱动功率输出噪声对偏置电流的影响中性作用失调电压漂移双极性差分放大器妥善地权衡了上述这些特性。它具有低直流失调、失调温漂和出色的动态性能,非常适合多种高分辨率、功能强大的数据采集和信号处理应用,这些应用通常需要使用驱动器来驱动ADC,如图1所示,由ADA4945-1驱动ADC。 ADA4945-1可配置多种功率模式,您可以在特定转换器上更好地权衡性能与功率。例如,在全功率模式下,可与配对,降低至低功耗模式后,可以适应或AD4022的低采样速率。图1.高分辨率数据采集系统的简化信号链示例作者简介Thomas Brand于2015年加入德国慕尼黑的ADI公司,当时他还在攻读硕士。毕业后,他参加了ADI公司的培训生项目。2017年,他成为一名现场应用工程师。Thomas为中欧的大型工业客户提供支持,并专注于工业以太网领域。他毕业于德国莫斯巴赫的联合教育大学电气工程专业,之后在德国康斯坦茨应用科学大学获得国际销售硕士学位。联系方式:
  • [技术干货] go语言monkey 组件迁移
    基于鲲鹏的Monkey补丁迁移 关于Monkey是基于Go 语言编写的一种补丁,它能在程序运行时插入汇编指令,使得调用目标函数时可以重定向到替换函数执行,同时能够取消插入的汇编指令,恢复对目标函数的正常调用。例如:有两函数 a, b,它们的实现分别如下:由图可知,a函数实现了一个 从 0到10的累加,b函数则直接返回一个 221在经过 monkey函数处理之后 monkey.patch(a, b)最后执行a函数,发现并没有执行从 0 到 10的累加,而是直接返回了221,也即最后执行的是b函数,起到了一个偷梁换柱的效果。Monkey设计当初是为x86架构设计的,未考虑Arm架构,且在 2015年停止了后续更新,即后续不会再推出适配Arm的版本。 项目背景某客户自研软件用到猴子补丁库 – Monkey, 软件在编译过程中报错,堆栈信息显示,编译过程中出现了不合法的地址:问题定位用devle工具进行单步调试追踪发现,在Monkey补丁的根目录下的monkey_386 .go 和monkey_amd64.go 文件中,均出现了x86的机器码:完整的机器码:问题根因:Monkey强行将x86的机器码硬编码进了程序中,从而导致在Arm架构的机器编译时无法正确识别,引发报错。 迁移思路迁移难点:从根因可知,Monkey的迁移,大概分为五个步骤:解构 go代码,将移位(byte(to >> XXXXXX))翻译成对应的机器码(16进制);翻译机器码,找到其对应的x86汇编代码,例如 0xFF,0x22 对应 jmp QWORD PTR [rdx] 将x86的汇编代码,翻译成Arm架构下所对应的汇编代码将Arm架构下的汇编代码,翻译成对应的Arm的机器码将Arm的机器码,改写成go对应的语句,并重新编译验证迁移思路:   迁移思路:Devkit工具扫描,看迁移建议。Exager: 将客户软件直接运行在Exager上直面问题,按五步法走 Devkit扫描:工具未给出任何迁移建议Exager 与 宿主机共用同一内核,pagesize为64K, 而原生 x86的go,是运行在4k上的,故go无法在 Exager上运行,除非修改pagesize为4k,但pagesize的修改有可能导致软件性能下降,客户不接受此方案。只能采用五步法为了更好地实施五步法,我们还需要了解Monkey的整体设计思路,做好全局的把控, Monkey的设计现有如下程序写法:执行变量f,发现执行的是a函数,是因为,a函数把其地址,赋值给了变量f对上面代码进行反汇编,可以发现:main.a.f加载到寄存器rdx里,然后把rdx寄存器指向的地址存入rbx里,最后调用。函数的地址值总是会加载到rdx寄存器里面,当代码调用的时候可以用来加载一些可能会用到的额外信息。回到开头,两函数,a, b 如何实现,调用a,实际上调用b?我们需要修改函数a,让它跳转到b的代码,跳过执行它自己的代码。实际上,我们需要通过这种方法来实现替换,加载函数b到寄存器rdx,然后执行时跳转到rdx上面。对应的x86汇编代码与机器码如下:其中:mov rdx是固定的,对应 48 C7 C2main.b.f 为函数地址,作为立即数,是动态变化的,用??取代,当立即数为0时,即为00,最大有8组,即64位立即数jmp [rdx] 也是固定的,对应的机器码为 FF 22因此我们可以得到如下类似函数:由此可知,动态变化的机器码,即为b函数地址,我们需要做的,就是将地址作为立即数,赋值给 rdx. X86与Arm的汇编差异了解了Monkey的设计思路后,我们再回看根目录下的 monkey_amd.64很明显,问题的关键,在于如何把to,进行拆分,并赋值给寄存器。X86的汇编很直接,寄存器能一口气存下 最大64位的立即数:即 movabs rdx, XX XX XX XX..  xx 每两个一组,每组8位,一共8组若立即数不够 64位,后面补0即可。即一个16位立即数,最后赋值会是 movabs rdx, xx xx 00 00 00 00 00 00 Arm则不通,最多一次只能存下 16位立即数,若立即数大于16位,比如64位,则必须拆分为4段。例如,现有立即数:0x8877665544332211对应机器码为 :1000100001110111011001100101010101000100001100110010001000010001从高位进行拆分,每16位拆分一段:第一段:1000100001110111011001100101010101000100001100110010001000010001即为:0x2211第二段:1000100001110111011001100101010101000100001100110010001000010001即为:0x4433第三段:1000100001110111011001100101010101000100001100110010001000010001即为:0x6655第四段:1000100001110111011001100101010101000100001100110010001000010001即为:0x8877综上,把一个0x8877665544332211 的立即数,赋值给一个寄存器(例如x10)最后的写法如下: 迁移实施综上。我们根据五步法,进行迁移尝试。第一步:解构 go代码,将移位(byte(to >> XXXXXX))翻译成对应的机器码(16进制)to是b函数地址,这里我们假设,其数值为0x8877665544332211 第二步:翻译机器码,找到其对应的x86汇编代码0x48, 0xBA, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66,0x77,0x88对应汇编:movabs rdx, 0x88776655443322110xFF, 0x22对应汇编:jmp QWORD PTR [rdx] 第三步:将x86的汇编代码,翻译成Arm架构下所对应的汇编代码movabs 我们用 mov movk 对应jmp 我们用 ldr 和 br进行对应故翻译过来就是:第四步:将Arm架构下的汇编代码,翻译成对应的Arm的机器码第五步:将Arm的机器码,改写成go对应的语句,并重新编译验证这两句是固定不变的0x4b, 0x01, 0x40, 0xf90x60, 0x01, 0x1f, 0xd6问题在于,如何将变化的部分,转换成go的代码呢?从机器码可以看出,变化的部分,仅仅只是红框框起来的部分,黑色部分是不变的。我们拿第一行进行举例:mov x10, 0x2211                 11010010100001000100001000101010我们将红色部分归零             11010010100000000000000000001010对应的16进制是:0xd280000a我们要做的,把0想象成一个个坑,我们要做的就是填坑,如何把0x2211 填到坑里面去从图中可知,头尾两部分,11010010100 和01010是固定不变的,需要保留,我们可以将0x2211 左移五位,把空位都留出来。即:00000000000001000100001000100000最后与 0xd280000a 相或即可,0与0相或为0, 0与1相或为1, 1与1相或也为1,这样,就做到了一个填坑的效果写成代码,即:  Arm64的cache一致性要让变更的机器码发挥作用,需要对内存进行修改,我们可以看到在后续的操作中。monkey通过copyToLocation 进行了内存修改操作X86架构从硬件方面保证了cache一致性,而Arm架构则需要通过软件进行保证,对内存修改操作后,需要对cache进行刷新,保证cache一致性。因此,我们需要在 copyToLocation中,进行cache刷新。修改copyToLocation函数如下:func copyToLocation(location uintptr, data []byte) {        f := rawMemoryAccess(location, len(data))        mprotectCrossPage(location, len(data), syscall.PROT_READ|syscall.PROT_WRITE|syscall.PROT_EXEC)        paddr := (*C.char)(&f[0])         copy(f, data[:])        C.dcache_flush(paddr, 64)         mprotectCrossPage(location, len(data), syscall.PROT_READ|syscall.PROT_EXEC)} 函数汇编指令过短问题从前面的论述我们得知,由于两种架构汇编指令得差异,同样的修改效果,x86架构所需汇编指令2条,Arm需要6条测试得知,当被替换函数过于简单,本身汇编指令条数小于6条时,且替换函数与被替换函数相邻时,会发生指令覆盖踩踏,导致错误。例如,有如下两个函数:no 是被替换函数,yes是替换函数它们的汇编指令如下:可以看到,no函数的指令条数是4, 小于6条当用monkey进行修改时,会发生如下现象no的汇编指令全被6条指令覆盖了,no本身只有4条,多出来的2条,把yes本身的前两条汇编指令也覆盖了,也即,yes函数的完整性遭到了破坏,因此br跳转时,会找不到yes函数的首地址,引发报错。规避措施:对于不在场景限制里的情况,无须考虑任何措施;对于处在场景限制里的情况,考虑对被替换函数增加空函数调用,如图1-11所示。
  • [技术干货] go语言monkey 组件迁移
    基于鲲鹏的Monkey补丁迁移 关于Monkey是基于Go 语言编写的一种补丁,它能在程序运行时插入汇编指令,使得调用目标函数时可以重定向到替换函数执行,同时能够取消插入的汇编指令,恢复对目标函数的正常调用。例如:有两函数 a, b,它们的实现分别如下:由图可知,a函数实现了一个 从 0到10的累加,b函数则直接返回一个 221在经过 monkey函数处理之后 monkey.patch(a, b)最后执行a函数,发现并没有执行从 0 到 10的累加,而是直接返回了221,也即最后执行的是b函数,起到了一个偷梁换柱的效果。Monkey设计当初是为x86架构设计的,未考虑Arm架构,且在 2015年停止了后续更新,即后续不会再推出适配Arm的版本。 项目背景某客户自研软件用到猴子补丁库 – Monkey, 软件在编译过程中报错,堆栈信息显示,编译过程中出现了不合法的地址:问题定位用devle工具进行单步调试追踪发现,在Monkey补丁的根目录下的monkey_386 .go 和monkey_amd64.go 文件中,均出现了x86的机器码:完整的机器码:问题根因:Monkey强行将x86的机器码硬编码进了程序中,从而导致在Arm架构的机器编译时无法正确识别,引发报错。 迁移思路迁移难点:从根因可知,Monkey的迁移,大概分为五个步骤:解构 go代码,将移位(byte(to >> XXXXXX))翻译成对应的机器码(16进制);翻译机器码,找到其对应的x86汇编代码,例如 0xFF,0x22 对应 jmp QWORD PTR [rdx] 将x86的汇编代码,翻译成Arm架构下所对应的汇编代码将Arm架构下的汇编代码,翻译成对应的Arm的机器码将Arm的机器码,改写成go对应的语句,并重新编译验证迁移思路:   迁移思路:Devkit工具扫描,看迁移建议。Exager: 将客户软件直接运行在Exager上直面问题,按五步法走 Devkit扫描:工具未给出任何迁移建议Exager 与 宿主机共用同一内核,pagesize为64K, 而原生 x86的go,是运行在4k上的,故go无法在 Exager上运行,除非修改pagesize为4k,但pagesize的修改有可能导致软件性能下降,客户不接受此方案。只能采用五步法为了更好地实施五步法,我们还需要了解Monkey的整体设计思路,做好全局的把控, Monkey的设计现有如下程序写法:执行变量f,发现执行的是a函数,是因为,a函数把其地址,赋值给了变量f对上面代码进行反汇编,可以发现:main.a.f加载到寄存器rdx里,然后把rdx寄存器指向的地址存入rbx里,最后调用。函数的地址值总是会加载到rdx寄存器里面,当代码调用的时候可以用来加载一些可能会用到的额外信息。回到开头,两函数,a, b 如何实现,调用a,实际上调用b?我们需要修改函数a,让它跳转到b的代码,跳过执行它自己的代码。实际上,我们需要通过这种方法来实现替换,加载函数b到寄存器rdx,然后执行时跳转到rdx上面。对应的x86汇编代码与机器码如下:其中:mov rdx是固定的,对应 48 C7 C2main.b.f 为函数地址,作为立即数,是动态变化的,用??取代,当立即数为0时,即为00,最大有8组,即64位立即数jmp [rdx] 也是固定的,对应的机器码为 FF 22因此我们可以得到如下类似函数:由此可知,动态变化的机器码,即为b函数地址,我们需要做的,就是将地址作为立即数,赋值给 rdx. X86与Arm的汇编差异了解了Monkey的设计思路后,我们再回看根目录下的 monkey_amd.64很明显,问题的关键,在于如何把to,进行拆分,并赋值给寄存器。X86的汇编很直接,寄存器能一口气存下 最大64位的立即数:即 movabs rdx, XX XX XX XX..  xx 每两个一组,每组8位,一共8组若立即数不够 64位,后面补0即可。即一个16位立即数,最后赋值会是 movabs rdx, xx xx 00 00 00 00 00 00 Arm则不通,最多一次只能存下 16位立即数,若立即数大于16位,比如64位,则必须拆分为4段。例如,现有立即数:0x8877665544332211对应机器码为 :1000100001110111011001100101010101000100001100110010001000010001从高位进行拆分,每16位拆分一段:第一段:1000100001110111011001100101010101000100001100110010001000010001即为:0x2211第二段:1000100001110111011001100101010101000100001100110010001000010001即为:0x4433第三段:1000100001110111011001100101010101000100001100110010001000010001即为:0x6655第四段:1000100001110111011001100101010101000100001100110010001000010001即为:0x8877综上,把一个0x8877665544332211 的立即数,赋值给一个寄存器(例如x10)最后的写法如下: 迁移实施综上。我们根据五步法,进行迁移尝试。第一步:解构 go代码,将移位(byte(to >> XXXXXX))翻译成对应的机器码(16进制)to是b函数地址,这里我们假设,其数值为0x8877665544332211 第二步:翻译机器码,找到其对应的x86汇编代码0x48, 0xBA, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66,0x77,0x88对应汇编:movabs rdx, 0x88776655443322110xFF, 0x22对应汇编:jmp QWORD PTR [rdx] 第三步:将x86的汇编代码,翻译成Arm架构下所对应的汇编代码movabs 我们用 mov movk 对应jmp 我们用 ldr 和 br进行对应故翻译过来就是:第四步:将Arm架构下的汇编代码,翻译成对应的Arm的机器码第五步:将Arm的机器码,改写成go对应的语句,并重新编译验证这两句是固定不变的0x4b, 0x01, 0x40, 0xf90x60, 0x01, 0x1f, 0xd6问题在于,如何将变化的部分,转换成go的代码呢?从机器码可以看出,变化的部分,仅仅只是红框框起来的部分,黑色部分是不变的。我们拿第一行进行举例:mov x10, 0x2211                 11010010100001000100001000101010我们将红色部分归零             11010010100000000000000000001010对应的16进制是:0xd280000a我们要做的,把0想象成一个个坑,我们要做的就是填坑,如何把0x2211 填到坑里面去从图中可知,头尾两部分,11010010100 和01010是固定不变的,需要保留,我们可以将0x2211 左移五位,把空位都留出来。即:00000000000001000100001000100000最后与 0xd280000a 相或即可,0与0相或为0, 0与1相或为1, 1与1相或也为1,这样,就做到了一个填坑的效果写成代码,即:  Arm64的cache一致性要让变更的机器码发挥作用,需要对内存进行修改,我们可以看到在后续的操作中。monkey通过copyToLocation 进行了内存修改操作X86架构从硬件方面保证了cache一致性,而Arm架构则需要通过软件进行保证,对内存修改操作后,需要对cache进行刷新,保证cache一致性。因此,我们需要在 copyToLocation中,进行cache刷新。修改copyToLocation函数如下:func copyToLocation(location uintptr, data []byte) {        f := rawMemoryAccess(location, len(data))        mprotectCrossPage(location, len(data), syscall.PROT_READ|syscall.PROT_WRITE|syscall.PROT_EXEC)        paddr := (*C.char)(&f[0])         copy(f, data[:])        C.dcache_flush(paddr, 64)         mprotectCrossPage(location, len(data), syscall.PROT_READ|syscall.PROT_EXEC)} 函数汇编指令过短问题从前面的论述我们得知,由于两种架构汇编指令得差异,同样的修改效果,x86架构所需汇编指令2条,Arm需要6条测试得知,当被替换函数过于简单,本身汇编指令条数小于6条时,且替换函数与被替换函数相邻时,会发生指令覆盖踩踏,导致错误。例如,有如下两个函数:no 是被替换函数,yes是替换函数它们的汇编指令如下:可以看到,no函数的指令条数是4, 小于6条当用monkey进行修改时,会发生如下现象no的汇编指令全被6条指令覆盖了,no本身只有4条,多出来的2条,把yes本身的前两条汇编指令也覆盖了,也即,yes函数的完整性遭到了破坏,因此br跳转时,会找不到yes函数的首地址,引发报错。规避措施:对于不在场景限制里的情况,无须考虑任何措施;对于处在场景限制里的情况,考虑对被替换函数增加空函数调用,如图1-11所示。
  • [技术干货] C语言程序设计全套知识点合集(物联网、单片机底层开发必备)
    一、前言在物联网系统开发中,离不开各种传感器、单片机的程序开发;一个简单的物联网系统构成应该是: 感知层 + 网络传输层 + 平台管理层 。具体而言:感知层:由各种各样的嵌入式设备构成。而单片机是嵌入式系统的硬件组成,是嵌入式系统软件运行的载体和必要条件;嵌入式系统在整个物联网框架下,更多的体现为感知层的角色,作为智能设备存在于整个物联网体系中,是物联网数据的采集来源。网络传输层:底层部分由嵌入式设备到平台的IoT网络构成,比较常见的网络主要有NB,Lora,Zigbee,GSM,LTE等。顶层部分是由网络到TCP/IP网络的转化。平台管理层:这部分主要完成不同数据传输协议的转化,实现数据的远程存储和设备远程管理。比如有的智能设备利用lwm2m协议传送数据,有的使用的是MQTT,还有的喜欢用coap封装数据。平台需要根据协议,完成上报数据的解析,设备的管理和命令的下发动作。从组成关系来讲,物联网由嵌入式设备构成,嵌入式设备中包含单片机。作为一名物联网工程师,如果主要是负责感知层的开发,那么C语言肯定是一项必须精通的语言。目前C语言还是单片机里的主流开发语言,这篇合集主要就是介绍C语言的知识点,从基本数据类型、变量、数组、指针、结构体顺序来介绍C语言的学习思路。二、C语言基础知识点2.1 C语言-基本数据类型与位运算https://bbs.huaweicloud.com/forum/forum.php?mod=viewthread&tid=190852这篇文章作为基础知识点,总结C语言的基本数据类型有哪些,浮点数的精度,整数变量的空间范围,变量定义语法,变量命名规则,浮点数打印格式,基本数据类型printf对应的打印、位运算的知识点。2.2 C语言-语句(if,for,while,switch,goto,return,break,continue)https://bbs.huaweicloud.com/forum/forum.php?mod=viewthread&tid=190854这篇文章作为C语言基础知识点,介绍C语言常用的几个语句的用法、规则、使用案例。介绍的语句如下: if..else 判断语句 for循环语句 while循环语句 do..while循环语句 switch 语句 goto 语句 return 语句 break 语句 continue 语句2.3 C语言-数组https://bbs.huaweicloud.com/forum/forum.php?mod=viewthread&tid=190858C语言的数组是一个同类型数据的集合,主要用来存储一堆同类型的数据。程序里怎么区分是数组?[ ] 这个括号是数组专用的符号. 定义数组、 访问数组数据都会用到。2.4 C语言-函数的定义、声明、传参https://bbs.huaweicloud.com/forum/forum.php?mod=viewthread&tid=190859C语言里函数是非常重要的知识点,一个完整的C语言程序就是由主函数和各个子函数组成的,主函数调用子函数完成各个逻辑功能。2.5 C语言-一维指针定义与使用https://bbs.huaweicloud.com/forum/forum.php?mod=viewthread&tid=190911指针在很多书本上都是当做重点来介绍,作为C语言的灵魂,项目里指针无处不在。比如: 指针作为函数形参的时候,可以间接修改源地址里的数据,也就相当于解决了函数return一次只能返回一个值的问题。指针在嵌入式、单片机里使用最直观,可以直接通过指针访问寄存器地址,对寄存器进行配置;计算机的CPU、外设硬件都是依靠地址操作的。2.6 C语言-内联函数、递归函数、指针函数https://bbs.huaweicloud.com/forum/forum.php?mod=viewthread&tid=190915这篇文章介绍C语言的内联函数、递归函数、函数指针、指针函数、局部地址、const关键字、extern关键字等知识点;这些知识点在实际项目开发中非常常用,非常重要。2.7 C语言-void类型作为万能指针类型https://bbs.huaweicloud.com/forum/forum.php?mod=viewthread&tid=190917void类型在基本数据类型里是空类型,无类型;void类型常用来当做函数的返回值,函数形参声明,表示函数没有返回值,没有形参。void类型不能用来定义变量,因为它是空类型–可以理解为空类型。2.8 C语言-指针作为函数形参类型https://bbs.huaweicloud.com/forum/forum.php?mod=viewthread&tid=191033C语言函数里最常用就是指针传参和返回地址,特别是字符串处理中,经常需要封装各种功能函数完成数据处理,并且C语言标准库里也提供了string.h 头文件,里面包含了很多字符串处理函数;这些函数的参数和返回值几乎都是指针类型。这篇文章就介绍如何使用指针作为函数参数、并且使用指针作为函数返回值。2.9 C语言-字符串处理https://bbs.huaweicloud.com/forum/forum.php?mod=viewthread&tid=191034字符串在C语言里使用非常多,因为很多数据处理都是文本,也就是字符串,特别是设备交互、web网页交互返回的几乎都是文本数据。字符串本身属于字符数组、只不过和字符数组区别是,字符串结尾有’\0’。 字符串因为规定结尾有'\0',在计算长度、拷贝、查找、拼接操作都很方便。2.10 C语言-结构体与位域https://bbs.huaweicloud.com/forum/forum.php?mod=viewthread&tid=191137C语言里的结构体是可以包含不同数据类型和相同数据类型的一个有序集合,属于构造类型,可以自己任意组合,并且结构体里也可以使用结构体类型作为成员。结构体在项目开发中使用非常多,无处不在,有了结构体类型就可以设计很多框架,模型,方便数据传输,存储等等。2.11 C语言-学生管理系统(结构体+数组实现)https://bbs.huaweicloud.com/forum/forum.php?mod=viewthread&tid=191138前面文章里介绍了结构体类型,知道结构体类型里可以存放不同的数据类型,属于一个有序的集合。这篇文章就使用结构体知识点完成一个小练习,使用结构体+数组设计一个简单的学生管理系统,作为结构体知识点的巩固练习。功能如下:(1). 欢迎界面提示(2). 输入密码登录(3). 功能: 录入学生信息、按照学号排序、按照成绩排序、输出所有学生信息、输出指定学生信息(学号、姓名、成绩)、计算成绩平均值值输出打印、删除指定学生信息、增加新的学生信息。(4). 功能模块采用菜单方式选择2.2 C语言-预处理(#define、#if...)https://bbs.huaweicloud.com/forum/forum.php?mod=viewthread&tid=191139在C语言程序里,出现的#开头的代码段都属于预处理。预处理:是在程序编译阶段就执行的代码段。比如: 包含头文件的的代码#include <stdio.h> #include <stdlib.h> #include <string.h>后续这篇帖子会持续更新,持续加入C语言后面的知识点,方便大家寻找对应的知识点。
  • [技术干货] 当你敲完Hello World后的第一步——C[转载]
    这里是目录前言一、#define指令1.#define定义宏2.#define 替换 宏3.带副作用的宏参数4.#undef撤销宏定义5.宏和函数对比(重点)二、条件编译指令1.单分支2.多分支条件编译3.判断某个符号是否被定义4.嵌套指令三、#include指令1.本地文件包含2.库文件包含3.嵌套文件包含前言了解敲完hello world后,编译器是怎么处理代码的第一步的呢,这是学习C和C++的基础。Hello World代码如下。放错了,重来。代码如下当你敲完Hello World这串代码时。编译器会对这些代码进行编译 和 链接的操作。而 编译: 又分为 预处理、编译、汇编。所以说 当你敲完C代码后的第一步,编译器会对C代码进行预处理.那么预处理主要做了那些事情呢?预处理大致做了以下事情:1.定义和替换由 #define指令定义的符号2.删除注释3.确定代码部分内容是否应该根据一些 条件编译指令 进行编译4.插入被 #include指令包含的内容所以本章详解预处理指令 #define、#include、条件编译指令。一、#define指令1.#define定义宏什么是宏?宏的定义:#define 允许把参数替换到文本中,这种实现通常称为宏或定义宏宏的声明格式:#define NAME stuff解释:没当有符号name出现在#define NAME stuff这条语句后面时,预处理器就会把它替换为 stuff。NAME:1.NAME是宏的名字。在这里 name 相当于变量,或者也可以相当于函数。但不等于函数!2.一般NAME都是大写,因为宏和函数语法很相似,语言本身我们无法区分,所以宏名要全部大写stuff:可以是常量。可以是表达式。也可以是一段程序。例如:以下代码在预处理后是什么样子呢?//定义声明宏//定义中我们使用了括号,这是一个好习惯,避免优先级的错误#define SQUARE(x)  (x)*(x)int main(){    printf("%d ", SQUARE(5));    return 0;}预处理后的代码,以下你看到的代码是编译器实实在在的处后的代码。#define SQUARE(x)  (x)*(x)int main(){    //将SQUARE(5)替换为(5)*(5)    printf("%d ", (5)*(5));    return 0;}你是否还对#define 替换迷惑?请继续往下看2.#define 替换 宏到底上面的代码是怎么替换的 宏?1.再调用宏时,首先对参数检查,看是否包含了#define定义的符号,比如SQUARE(5),然后将它的x * x替换为5 * 5.2.对于宏,参数名被他们的值所替代。3.最后,再次对文本扫描,看是否包含了热任何由#define定义的符号。如果是,就重复上述处理过程。为什么会有第3步的重复呢?因为有时候#define定义可以包含其他#define定义的符号。但是宏不可以递归!3.带副作用的宏参数什么是带副作用的宏参数?副作用:就是表达式求值的时候出现的永久性效果。例如:x+1;//不带副作用x++;//带有副作用12下面代码输出结果是什么?#include <stdio.h>#define ADD(a, b) (a)+(b)int main(){    int x = 2;     int y = 3;     int z = ADD(x++, y++);    //输出的结果是什么?    //x=3 y=4 z=5    printf("x=%d y=%d z=%d\n", x, y, z);    return 0;}因为被替换的代码是int z = ADD(x++, y++);替换后为:int z = (x++)+(y++);这样结果就一目了然。4.#undef撤销宏定义#undef:这条指令用于移除一个宏定义例如:移除MAX这个宏。5.宏和函数对比(重点)属性    #define定义宏    函数代码长度    每次使用时,宏代码都会被插入到程序中。除了非常小的宏之外,程序的长度会大幅度增长    函数代码只出现于一个地方。每次使用函数时,都调用同一个地方的代码执行速度    更快    存在函数的调用和返回 的格外开销,所以相对慢一些操作符优先级    宏参数求值需要加上括号,否则容易造成不可以预料的后果    只在函数调用事求值一次,不会带副作用带有副作用的参数    参数可能被替换到宏的多个位置,有的可能带有副作用    函数参数只在传参的时候求值一次,结果更容易控制参数类型    宏的参数与类型无关,可以是任何类型的的参数    函数参数与类型有关,参数类型不同就需要不同的函数,因为C语言没有C++的重载调试    宏是不可以调试的,因为在程序运行前就已经替换的宏    可以逐语句调试二、条件编译指令什么是条件编译?意思就是我们可以选择性的编译。条件编译:你可以选择代码的一部分是被正常编译还是完全忽略。用于支持条件编译的基本结构是#if指令和与其匹配的#endif指令。1.单分支常量表达式expression,由预处理器求值。如果expression为真,那么statements将被执行,否则预处理器就安静的删除它们。#if expression statements;#endif//常量表达式expression,由预处理器求值。2.多分支条件编译同if else语句,为真则执行。#if expression //...#elif expression //...#else //...#endif3.判断某个符号是否被定义为了测试一个符号是否已经被定义。在条件编译中完成这个任务更方便。以下两条语句功能想通过。1.#if defined(symbol)2.#ifdef symbol4.嵌套指令某个程序既要在windows系统下能够运行,也需要在Linux系统下运行,这就要条件编译来解决跨平台问题。这时候嵌套指令很容易解决。#if defined(OS_UNIX)     #ifdef OPTION1             unix_version_option1();     #endif     #ifdef OPTION2             unix_version_option2();     #endif#elif defined(OS_MSDOS)     #ifdef OPTION2             msdos_version_option2();     #endif#endif三、#include指令#include在预处理时会被展开。这种展开的方式很简单:1.预处理器先删除这条指令,并用**#include**所包含文件的内容替换。2.这样一个源文件被包含10次,那就实际被编译10次。1.本地文件包含#include "Add.h"1查找方法:1.先在源文件所在目录下查找2.如果该头文件未找到,编译器就像查找库函数头文件一样在标准位置查找头文件。2.库文件包含#include <stdio.h>1查找方法:查找头文件直接去标准路径下去查找,如果找不到就提示编译错误。这样是不是可以说,对于库文件也可以使用 “” 的形式包含?答案是肯定的,可以。但是这样做查找的效率就低些,当然这样也不容易区分是库文件还是本地文件了。3.嵌套文件包含有时候会重复包含头文件,以前为了解决这个方法,人们用了条件编译。代码如下每个头文件的开头写:例如有个test.h的头文件。用下划线分开头文件。全大写。#ifndef __TEST_H__#define __TEST_H__//这里面写头文件的内容#endif  上面这种写法比较古老。现在一般用这个写法#pragma once#pragma once也是是用来防止头文件被包含的。原文链接:https://blog.csdn.net/qq2466200050/article/details/124615042
  • [技术干货] 如何编写【GPIO】设备的驱动程序?
    目录示例程序目标编写驱动程序编写应用程序卸载驱动模块别人的经验,我们的阶梯!大家好,我是道哥。在前几篇文章中,我们一块讨论了:在 Linux 系统中,编写字符设备驱动程序的基本框架,主要是从代码流程和 API 函数这两方面触发。这篇文章,我们就以此为基础,写一个有实际应用功能的驱动程序:1. 在驱动程序中,初始化 GPIO 设备,自动创建设备节点;2. 在应用程序中,打开 GPIO 设备,并发送控制指令设置 GPIO 口的状态;示例程序目标编写一个驱动程序模块:mygpio.ko。当这个驱动模块被加载的时候,在系统中创建一个 mygpio 类设备,并且在 /dev 目录下,创建 4 个设备节点:/dev/mygpio0/dev/mygpio1/dev/mygpio2/dev/mygpio3因为我们现在是在 x86 平台上来模拟 GPIO 的控制操作,并没有实际的 GPIO 硬件设备。因此,在驱动代码中,与硬件相关部分的代码,使用宏 MYGPIO_HW_ENABLE 控制起来,并且在其中使用printk输出打印信息来体现硬件的操作。在应用程序中,可以分别打开以上这 4 个 GPIO 设备,并且通过发送控制指令,来设置 GPIO 的状态。编写驱动程序以下所有操作的工作目录,都是与上一篇文章相同的,即:~/tmp/linux-4.15/drivers/。创建驱动目录和驱动程序$ cd linux-4.15/drivers/$ mkdir mygpio_driver$ cd mygpio_driver$ touch mygpio.cmygpio.c 文件的内容如下(不需要手敲,文末有代码下载链接):相对于前几篇文章来说,上面的代码稍微有一点点复杂,主要是多了宏定义 MYGPIO_HW_ENABLE 控制部分的代码。比如:在这个宏定义控制下的三个与硬件相关的函数:gpio_hw_init()gpio_hw_release()gpio_hw_set()就是与GPIO硬件的初始化、释放、状态设置相关的操作。代码中的注释已经比较完善了,结合前几篇文章中的函数说明,还是比较容易理解的。从代码中可以看出:驱动程序使用 alloc_chrdev_region 函数,来动态注册设备号,并且利用了 Linux 应用层中的 udev 服务,自动在 /dev 目录下创建了设备节点。另外还有一点:在上面示例代码中,对设备的操作函数只实现了 open 和 ioctl 这两个函数,这是根据实际的使用场景来决定的。这个示例中,只演示了如何控制 GPIO 的状态。你也可以稍微补充一下,增加一个read函数,来读取某个GPIO口的状态。控制 GPIO 设备,使用 write 或者 ioctl 函数都可以达到目的,只是 ioctl 更灵活一些。创建 Makefile 文件$ touch Makefile内容如下:编译驱动模块$ make得到驱动程序: mygpio.ko 。加载驱动模块在加载驱动模块之前,先来检查一下系统中,几个与驱动设备相关的地方。先看一下 /dev 目录下,目前还没有设备节点( /dev/mygpio[0-3] )。$ ls -l /dev/mygpio*ls: cannot access '/dev/mygpio*': No such file or directory再来查看一下 /proc/devices 目录下,也没有 mygpio 设备的设备号。$ cat /proc/devices为了方便查看打印信息,把dmesg输出信息清理一下:$ sudo dmesg -c现在来加载驱动模块,执行如下指令:$ sudo insmod mygpio.ko当驱动程序被加载的时候,通过 module_init( ) 注册的函数 gpio_driver_init() 将会被执行,那么其中的打印信息就会输出。还是通过 dmesg 指令来查看驱动模块的打印信息:$ dmesg可以看到:操作系统为这个设备分配的主设备号是 244,并且也打印了GPIO硬件的初始化函数的调用信息。此时,驱动模块已经被加载了!来查看一下 /proc/devices 目录下显示的设备号:$ cat /proc/devices设备已经注册了,主设备号是: 244 。设备节点由于在驱动程序的初始化函数中,使用 cdev_add 和 device_create 这两个函数,自动创建设备节点。所以,此时我们在 /dev 目录下,就可以看到下面这4个设备节点:现在,设备的驱动程序已经加载了,设备节点也被创建好了,应用程序就可以来控制 GPIO 硬件设备了。应用程序应用程序仍然放在 ~/tmp/App/ 目录下。$ mkdir ~/tmp/App/app_mygpio$ cd ~/tmp/App/app_mygpio$ touch app_mygpio.c文件内容如下:以上代码也不需要过多解释,只要注意参数的顺序即可。接下来就是编译和测试了:$ gcc app_mygpio.c -o app_mygpio执行应用程序的时候,需要携带2个参数:GPIO 设备编号(0 ~ 3),设置的状态值(0 或者 1)。这里设置一下/dev/mygpio0这个设备,状态设置为1:$ sudo ./app_mygpio 0 1[sudo] password for xxx: <输入用户密码>/dev/mygpio0: open success!如何确认/dev/mygpio0这个GPIO的状态确实被设置为1了呢?当然是看 dmesg 指令的打印信息:$ dmesg通过以上打印信息可以看到:确实执行了【设置 mygpio0 的状态为 1】的动作。再继续测试一下:设置 mygpio0 的状态为 0:$ sudo ./app_mygpio 0 0当然了,设置其他几个GPIO口的状态,都是可以正确执行的!卸载驱动模块卸载指令:$ sudo rmmod mygpio此时,/proc/devices 下主设备号 244 的 mygpio 已经不存在了。再来看一下 dmesg的打印信息:可以看到:驱动程序中的 gpio_driver_exit( ) 被调用执行了。并且,/dev 目录下的 4 个设备节点,也被函数 device_destroy() 自动删除了!
  • [问题求助] stm32l431rct6单片机坏了
    开发板和电脑连接是亮灯的,但单片机突然发烫,再重启,lcd屏也是黑屏状态了,怎么修呢
  • [技术干货] Linux驱动开发_倒车影像项目介绍[转载]
    原文链接:https://bbs.huaweicloud.com/blogs/353235倒车影像项目模拟: 汽车中控台---倒车影像。组成部分:1.​ LCD屏: 实时显示摄像头采集的数据。2.​ 摄像头: 放在车尾,采集图像传输给LCD屏进行显示。3.​ 倒车雷达: 超声波测距--->测量车尾距离障碍物的距离。4.​ 蜂鸣器: 根据倒车雷达测量的距离,控制频率。1.1 超声波测距模块声波测距: 已知声音在空气中传播的速度。​ 硬件接线:ECHO------->GPX1_0 (开发板第9个IO口): 中断引脚----->检测回波----输入TRIG ------->GPB_7 (开发板第8个IO口): 输出触发信号。1.2 PWM方波控制蜂鸣器​ PWM方波:​ 内核自带的PWM方波驱动1.3 UVC免驱摄像头编程框架: V4L2编程的框架: v4l2--->全称: video4linux2V4L2 : 针对UVC免驱USB设备设计框架。专用于USB摄像头的数据采集。免驱 : 驱动已经成为标准,属于内核自带源码的一部分。V4L2框架本身注册的也是字符设备,设备节点: /dev/videoXV4L2 框架: 提供ioctl接口,提供了有很多命令,可以通过这些命令对摄像头做配置。比如: 输出的图像尺寸,输出图像格式(RGB、YUV、JPG),申请采集数据的缓冲区。​ 配置摄像头采集队列步骤:Mmap函数映射。超声波驱动读取距离:#include <linux/kernel.h> #include <linux/module.h> #include <linux/miscdevice.h> #include <linux/fs.h> #include <linux/uaccess.h> #include <linux/io.h> #include <linux/irq.h> #include <linux/delay.h> #include <linux/workqueue.h> #include <linux/gpio.h> #include <mach/gpio.h> #include <plat/gpio-cfg.h> #include <linux/timer.h> #include <linux/wait.h> #include <linux/sched.h> #include <linux/poll.h> #include <linux/fcntl.h> #include <linux/interrupt.h> #include <linux/ktime.h> static unsigned int distance_irq; /*存放中断号*/ static u32 *GPB_DAT=NULL; static u32 *GPB_CON=NULL; static u32 distance_time_us=0; /*表示距离的时间*/ /* 工作队列处理函数: */ static void distance_work_func(struct work_struct *work) { u32 time1,time2; time1=ktime_to_us(ktime_get()); /*获取当前时间,再转换为 us 单位*/ /*等待高电平时间结束*/ while(gpio_get_value(EXYNOS4_GPX1(0))){} time2=ktime_to_us(ktime_get()); /*获取当前时间,再转换为 us 单位*/ distance_time_us=time2-time1; //printk("us=%d\n",time2-time1); /*us/58=厘米*/ } /*静态方式初始化工作队列*/ static DECLARE_WORK(distance_work,distance_work_func); /* 中断处理函数: 用于检测超声波测距的回波 */ static irqreturn_t distance_handler(int irq, void *dev) { /*调度工作队列*/ schedule_work(&distance_work); return IRQ_HANDLED; } static void distance_function(unsigned long data); /*静态方式定义内核定时器*/ static DEFINE_TIMER(distance_timer,distance_function,0,0); /*内核定时器超时处理函数: 触发超声波发送方波*/ static void distance_function(unsigned long data) { static u8 state=0; state=!state; /*更改GPIO口电平*/ if(state) { *GPB_DAT|=1<<7; } else { *GPB_DAT&=~(1<<7); } /*修改定时器的超时时间*/ mod_timer(&distance_timer,jiffies+msecs_to_jiffies(100)); } static int distance_open(struct inode *inode, struct file *file) { return 0; } #define GET_US_TIME 0x45612 static long distance_unlocked_ioctl(struct file *file, unsigned int cmd, unsigned long argv) { u32 *us_data=(u32*)argv; int err; u32 time_us=distance_time_us; switch(cmd) { case GET_US_TIME: err=copy_to_user(us_data,&time_us,4); if(err!=0)printk("拷贝失败!\n"); break; } return 0; } static int distance_release(struct inode *inode, struct file *file) { return 0; } /*定义文件操作集合*/ static struct file_operations distance_fops= { .open=distance_open, .unlocked_ioctl=distance_unlocked_ioctl, .release=distance_release }; /*定义杂项设备结构体*/ static struct miscdevice distance_misc= { .minor=MISC_DYNAMIC_MINOR, .name="tiny4412_distance", .fops=&distance_fops }; static int __init tiny4412_distance_dev_init(void) { int err; /*1. 映射GPIO口地址*/ GPB_DAT=ioremap(0x11400044,4); GPB_CON=ioremap(0x11400040,4); *GPB_CON&=~(0xF<<4*7); *GPB_CON|=0x1<<4*7; /*配置输出模式*/ /*2. 根据GPIO口编号,获取中断号*/ distance_irq=gpio_to_irq(EXYNOS4_GPX1(0)); /*3. 注册中断*/ err=request_irq(distance_irq,distance_handler,IRQ_TYPE_EDGE_RISING,"distance_device",NULL); if(err!=0)printk("中断注册失败!\n"); else printk("中断:超声波测距驱动安装成功!\n"); /*4. 修改定时器超时时间*/ mod_timer(&distance_timer,jiffies+msecs_to_jiffies(100)); /*杂项设备注册*/ misc_register(&distance_misc); return 0; } static void __exit tiny4412_distance_dev_exit(void) { /*5. 注销中断*/ free_irq(distance_irq,NULL); /*6. 停止定时器*/ del_timer(&distance_timer); /*7. 取消IO映射*/ iounmap(GPB_DAT); iounmap(GPB_CON); /*注销杂项设备*/ misc_deregister(&distance_misc); printk("中断:超声波测距驱动卸载成功!\n"); } module_init(tiny4412_distance_dev_init); module_exit(tiny4412_distance_dev_exit); MODULE_LICENSE("GPL"); MODULE_AUTHOR("tiny4412 wbyq");摄像头代码,读取摄像头画面:#include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <sys/ioctl.h> #include <poll.h> #include <signal.h> #include <unistd.h> #include <fcntl.h> #include <poll.h> #include <signal.h> #include <pthread.h> #include <linux/videodev2.h> #include <stdlib.h> #include <sys/mman.h> #include <string.h> #include "framebuffer.h" #define PWM_DEVICE "/dev/pwm" /*PWM方波设备文件*/ #define DISTANCE_DEVICE "/dev/tiny4412_distance" /*超声波测距设备文件*/ #define UVC_VIDEO_DEVICE "/dev/video15" /*UVC摄像头设备节点*/ #define GET_US_TIME 0x45612 /*获取超声波测量的距离: ioctl命令*/ #define PWM_IOCTL_SET_FREQ 1 /*控制PWM方波频率: ioctl命令*/ #define PWM_IOCTL_STOP 0 /*停止PWM方波输出: ioctl命令*/ int distance_fd; /*超声波设备的文件描述符*/ int pwm_fd; /*PWM方波设备的文件描述符*/ int uvc_video_fd; /*UVC摄像头设备文件描述符*/ int Image_Width; /*图像的宽度*/ int Image_Height; /*图像的高度*/ unsigned char *video_memaddr_buffer[4]; /*存放摄像头映射到进程空间的缓冲区地址*/ /* 函数功能: 用户终止了进程调用 */ void exit_sighandler(int sig) { //停止PWM波形输出,关闭蜂鸣器 ioctl(pwm_fd,PWM_IOCTL_STOP,0); close(pwm_fd); close(distance_fd); exit(1); } /* 函数功能: 读取超声波数据的线程 */ void *distance_Getpthread_func(void *dev) { /*1. 打开PWM方波驱动*/ pwm_fd=open(PWM_DEVICE,O_RDWR); if(pwm_fd<0) //0 1 2 { printf("%s 设备文件打开失败\n",PWM_DEVICE); /*退出线程*/ pthread_exit(NULL); } /*2. 打开超声波测距设备*/ distance_fd=open(DISTANCE_DEVICE,O_RDWR); if(distance_fd<0) //0 1 2 { printf("%s 设备文件打开失败\n",DISTANCE_DEVICE); /*退出线程*/ pthread_exit(NULL); } /*3. 循环读取超声波测量的距离*/ struct pollfd fds; fds.fd=distance_fd; fds.events=POLLIN; int data; while(1) { poll(&fds,1,-1); ioctl(distance_fd,GET_US_TIME,&data); printf("距离(cm):%0.2f\n",data/58.0); data=data/58; if(data>200) /*200厘米: 安全区域*/ { //停止PWM波形输出,关闭蜂鸣器 ioctl(pwm_fd,PWM_IOCTL_STOP,0); } else if(data>100) /*100厘米: 警告区域*/ { printf("警告区域!\n"); ioctl(pwm_fd,PWM_IOCTL_SET_FREQ,2); } else /*小于<100厘米: 危险区域*/ { printf(" 危险区域!\n"); ioctl(pwm_fd,PWM_IOCTL_SET_FREQ,10); } //ioctl(pwm_fd,PWM_IOCTL_SET_FREQ,pwm_data); /*倒车影像: 测距有3个档位*/ } } /* 函数功能: UVC摄像头初始化 返回值: 0表示成功 */ int UVCvideoInit(void) { /*1. 打开摄像头设备*/ uvc_video_fd=open(UVC_VIDEO_DEVICE,O_RDWR); if(uvc_video_fd<0) { printf("%s 摄像头设备打开失败!\n",UVC_VIDEO_DEVICE); return -1; } /*2. 设置摄像头的属性*/ struct v4l2_format format; memset(&format,0,sizeof(struct v4l2_format)); format.type=V4L2_BUF_TYPE_VIDEO_CAPTURE; /*表示视频捕获设备*/ format.fmt.pix.width=800; /*预设的宽度*/ format.fmt.pix.height=480; /*预设的高度*/ format.fmt.pix.pixelformat=V4L2_PIX_FMT_YUYV; /*预设的格式*/ format.fmt.pix.field=V4L2_FIELD_ANY; /*系统自动设置: 帧属性*/ if(ioctl(uvc_video_fd,VIDIOC_S_FMT,&format)) /*设置摄像头的属性*/ { printf("摄像头格式设置失败!\n"); return -2; } Image_Width=format.fmt.pix.width; Image_Height=format.fmt.pix.height; printf("摄像头实际输出的图像尺寸:x=%d,y=%d\n",format.fmt.pix.width,format.fmt.pix.height); if(format.fmt.pix.pixelformat==V4L2_PIX_FMT_YUYV) { printf("当前摄像头支持YUV格式图像输出!\n"); } else { printf("当前摄像头不支持YUV格式图像输出!\n"); return -3; } /*3. 请求缓冲区: 申请摄像头数据采集的缓冲区*/ struct v4l2_requestbuffers req_buff; memset(&req_buff,0,sizeof(struct v4l2_requestbuffers)); req_buff.count=4; /*预设要申请4个缓冲区*/ req_buff.type=V4L2_BUF_TYPE_VIDEO_CAPTURE; /*视频捕获设备*/ req_buff.memory=V4L2_MEMORY_MMAP; /*支持mmap内存映射*/ if(ioctl(uvc_video_fd,VIDIOC_REQBUFS,&req_buff)) /*申请缓冲区*/ { printf("申请摄像头数据采集的缓冲区失败!\n"); return -4; } printf("摄像头缓冲区申请的数量: %d\n",req_buff.count); /*4. 获取缓冲区的详细信息: 地址,编号*/ struct v4l2_buffer buff_info; memset(&buff_info,0,sizeof(struct v4l2_buffer)); int i; for(i=0;i<req_buff.count;i++) { buff_info.type=V4L2_BUF_TYPE_VIDEO_CAPTURE; /*视频捕获设备*/ buff_info.memory=V4L2_MEMORY_MMAP; /*支持mmap内存映射*/ if(ioctl(uvc_video_fd,VIDIOC_QUERYBUF,&buff_info)) /*获取缓冲区的详细信息*/ { printf("获取缓冲区的详细信息失败!\n"); return -5; } /*根据摄像头申请缓冲区信息: 使用mmap函数将内核的地址映射到进程空间*/ video_memaddr_buffer[i]=mmap(NULL,buff_info.length,PROT_READ|PROT_WRITE,MAP_SHARED,uvc_video_fd,buff_info.m.offset); if(video_memaddr_buffer[i]==NULL) { printf("缓冲区映射失败!\n"); return -6; } } /*5. 将缓冲区放入采集队列*/ memset(&buff_info,0,sizeof(struct v4l2_buffer)); for(i=0;i<req_buff.count;i++) { buff_info.type=V4L2_BUF_TYPE_VIDEO_CAPTURE; /*视频捕获设备*/ buff_info.index=i; /*缓冲区的节点编号*/ buff_info.memory=V4L2_MEMORY_MMAP; /*支持mmap内存映射*/ if(ioctl(uvc_video_fd,VIDIOC_QBUF,&buff_info)) /*根据节点编号将缓冲区放入队列*/ { printf("根据节点编号将缓冲区放入队列失败!\n"); return -7; } } /*6. 启动摄像头数据采集*/ int Type=V4L2_BUF_TYPE_VIDEO_CAPTURE; if(ioctl(uvc_video_fd,VIDIOC_STREAMON,&Type)) { printf("启动摄像头数据采集失败!\n"); return -8; } return 0; } /* 函数功能: 将YUV数据转为RGB格式 函数参数: unsigned char *yuv_buffer: YUV源数据 unsigned char *rgb_buffer: 转换之后的RGB数据 int iWidth,int iHeight : 图像的宽度和高度 */ void yuv_to_rgb(unsigned char *yuv_buffer,unsigned char *rgb_buffer,int iWidth,int iHeight) { int x; int z=0; unsigned char *ptr = rgb_buffer; unsigned char *yuyv= yuv_buffer; for (x = 0; x < iWidth*iHeight; x++) { int r, g, b; int y, u, v; if (!z) y = yuyv[0] << 8; else y = yuyv[2] << 8; u = yuyv[1] - 128; v = yuyv[3] - 128; r = (y + (359 * v)) >> 8; g = (y - (88 * u) - (183 * v)) >> 8; b = (y + (454 * u)) >> 8; *(ptr++) = (r > 255) ? 255 : ((r < 0) ? 0 : r); *(ptr++) = (g > 255) ? 255 : ((g < 0) ? 0 : g); *(ptr++) = (b > 255) ? 255 : ((b < 0) ? 0 : b); if(z++) { z = 0; yuyv += 4; } } } int main(int argc,char **argv) { int data; /*1. 注册将要捕获的信号*/ signal(SIGINT,exit_sighandler); /*2. 创建线程: 采集超声波测量的距离*/ pthread_t threadID; pthread_create(&threadID,NULL,distance_Getpthread_func,NULL); pthread_detach(threadID); //设置分离属性 /*3. 初始化摄像头*/ UVCvideoInit(); /*4. 初始化LCD屏*/ framebuffer_Device_init(); /*5. 循环采集摄像头的数据*/ struct pollfd fds; fds.fd=uvc_video_fd; fds.events=POLLIN; struct v4l2_buffer buff_info; memset(&buff_info,0,sizeof(struct v4l2_buffer)); int index=0; /*表示当前缓冲区的编号*/ unsigned char *rgb_buffer=NULL; /*申请空间:存放转换之后的RGB数据*/ rgb_buffer=malloc(Image_Width*Image_Height*3); if(rgb_buffer==NULL) { printf("RGB转换的缓冲区申请失败!\n"); exit(0); } while(1) { /*1. 等待摄像头采集数据*/ poll(&fds,1,-1); /*2. 取出一帧数据: 从采集队列里面取出一个缓冲区*/ buff_info.type=V4L2_BUF_TYPE_VIDEO_CAPTURE; /*视频捕获设备*/ ioctl(uvc_video_fd,VIDIOC_DQBUF,&buff_info); /*从采集队列取出缓冲区*/ index=buff_info.index; //printf("采集数据的缓冲区的编号:%d\n",index); /*3. 处理数据: YUV转RGB\显示到LCD屏*/ //video_memaddr_buffer[index]; /*当前存放数据的缓冲区地址*/ /*3.1 将YUV数据转为RGB格式*/ yuv_to_rgb(video_memaddr_buffer[index],rgb_buffer,Image_Width,Image_Height); /*3.2 将RGB数据实时刷新到LCD屏幕上*/ framebuffer_DisplayImages((800-Image_Width)/2,0,Image_Width,Image_Height,rgb_buffer); /*4. 将缓冲区再次放入采集队列*/ buff_info.memory=V4L2_MEMORY_MMAP; /*支持mmap内存映射*/ buff_info.type=V4L2_BUF_TYPE_VIDEO_CAPTURE; /*视频捕获设备*/ buff_info.index=index; /*缓冲区的节点编号*/ ioctl(uvc_video_fd,VIDIOC_QBUF,&buff_info); /*根据节点编号将缓冲区放入队列*/ } return 0; }
  • [技术干货] GB2312、GBK汉字取模与字库偏移地址的计算-单片机上常用
    一、 GB2312编码GB2312 码是中华人民共和国国家汉字信息交换用编码,全称《信息交换用汉字编码字符集--基本集》, 由国家标准总局发布, 1981 年 5 月 1 日 实施,通行于大陆。新加坡等地也使用此编码。GB2312 收录简化汉字及符号、字母、 日文假名等共 7445 个图形字符,其中汉字占 6763 个。GB2312 规定<对任意一个图形字符都采用两个字节表示,每个字节均采用七位编码表示>,习惯上称第一个字节为<高字节>, 第二个字节为<低字节>。GB2312-80 包含了大部分常用的一、二级汉字, 和 9 区的符号。该字符集是几乎所有的中文系统和国际化的软件都支持的中文字符集,这也是最基本的中文字符集。其编码范围是高位 0xa1- 0xfe, 低位也是 0xa1-0xfe;汉字从 0xb0a1 开始,结束于 0xf7fe。GB2312 将代码表分为 94 个区,对应第一字节( 0xa1 -0xfe);每个区 94 个位(0xa1-0xfe),对应第二字节,两个字节的值分别为区号值和位号值加 32(20H), 因此也称为区位码。01-09 区为符号、数字区, 16-87 区为汉字区(0xb0-0xf7),10-15 区、88-94 区是有待进一步标准化的空白区。 GB2312 将收录的汉字分成两级: 第一级是常用汉字计 3755 个,置于 1 6-55 区, 按汉语拼音字母/笔形顺序排列;第二级汉字是次常用汉字计 3008 个,置于 56-87 区,按部首/笔画顺序排列。故而 GB2312 最多能表示 6763 个汉字。二、GBK编码全国信息技术化技术委员会于 1995 年 12 月 1 日《汉字内码扩展规范》。GBK 向下与 GB2312 完全兼容,向上支持 ISO 10646 国际标准,在前者向后者过渡过程中起到的承上启下的作用。GBK 亦采用双字节表示,总体编码范围为 8140-FEFE 之间,首字节在 81-FE 之间,尾字节在 40-FE 之间,剔除 XX7F 一条线。 GBK 共收入 21886 个汉字和图形符号,包括: GB2312 中的全部汉字、非汉字符号。 BIG5 中的全部汉字。 与 ISO 10646 相应的国家标准 GB13000 中的其它 CJK 汉字,以上合计 20902 个汉字。 其它汉字、部首、符号,共计 984 个。 GBK 编码区分三部分: 1) 汉字区,包括: GBK/2:OXBOA1-F7FE, 收录 GB2312 汉字 6763 个,按原序排列; GBK/3:OX8140-AOFE,收录 CJK 汉字 6080 个; GBK/4:OXAA40-FEAO,收录 CJK 汉字和增补的汉字 8160 个。 2) 图形符号区,包括: GBK/1:OXA1A1-A9FE,除 GB2312 的符号外,还增补了其它符号 GBK/5:OXA840-A9AO,扩除非汉字区。 3) 用户自定义区: 即 GBK 区域中的空白区,用户可以自己定义字符  每个 GBK 码由 2 个字节组成:第一个字节为 0X81~0XFE第二个字节分为两部分:0X40~0X7E2.0X80~0XFE。其中与 GB2312 相同的区域,字完全相同。 我们把第一个字节代表的意义称为区,那么 GBK 里面总共有 126 个区( 0XFE-0X81+1),每个区内有 190 个汉字( 0XFE-0X80+0X7E-0X40+2),总共就有 126*190=23940 个汉字。我们的点阵库只要按照这个编码规则从 0X8140 开始,逐一建立, 每个区的点阵大小为每个汉字所用的字节数*190。这样,我们就可以得到在这个字库里面定位汉字的方法: 当 GBKL<0X7F 时: Hp=((GBKH-0x81)*190+GBKL-0X40)*(size*2); 当 GBKL>0X80 时: Hp=((GBKH-0x81)*190+GBKL-0X41)*(size*2);其中 GBKH、 GBKL 分别代表 GBK 的第一个字节和第二个字节(也就是高位和低位), size代表汉字字体的大小(比如 16 字体, 12 字体等), Hp 则为对应汉字点阵数据在字库里面的起始地址(假设是从 0 开始存放)。这样我们只要得到了汉字的 GBK 码,就可以显示这个汉字了。从而实现汉字在液晶上的显示。简化公式:                 if(L<0x7f)L=L-0x40;        else L=L-0x41;        H=H-0x81;           Addr=(190*H+L)*size;L 是汉字的低字节,H是汉字的高字节。Addr 是该汉字在字库里的偏移量。Size 是该汉字的应点阵集所占的字节数量。 汉字的高字节大于0x80 ,才是汉字。高字节小于0X80就是英文字符。字库在FLASH寻址过程:首先得到该汉字点阵码在FLASH里的存储偏移量,然后在加上该汉字库在FLASH里的存放起始地址,就得到了该汉字的点阵数据位置。得到绝对位置之后,就可以读出点阵码,进行打点显示。 BIG5 编码 BIG5 是通行于台湾、香港地区的一个繁体字编码方案。虽然存在一些瑕疵,但广泛应用于电脑行业,尤其是互联网中,从而成为一种事实上的行业标准。 1983 年 10 月,台湾国家科学委员会、教育部国语推行委员会、中央标准局、行政院共同制定了《通用汉字标准交换码》,后经修订于 1992 年 5 月公布,更名为《中文标准交换码》,BIG5 是台湾资讯工业策进会根据以上标准制定的编码方案。 BIG5 码是双字节编码方案,其中第一个字节的值在 OXAO-OXFE 之间,第二个字节在 OX40-OX7E 和 OXA1-OXFE 之间。 BIG5 收录 13461 个汉字和符号,包括: 符号 408 个,编码位置 A140-A3BE 常用字 5401 个,编码位置 A440-C67E,包括台湾教育部颁布的《常用国字标准字体表》的全部汉字 4808 个,台湾教科书常用字 587 个,异体字 6 个。 次常用字 7652 个,编码位置 C940-F9D5,包括台湾教育部颁布的《次常用国字标准字体表》的全部汉字 6341 个,《罕用国字标准字体表》中使用频率较高的字 1311 个。 GB13000 编码 GB13000 等同于国际标准的《通用多八位编码字符集 (UCS)》 ISO10646.1,就是等同于 Unicode 的标准,代码页等等的都使用 UTF 的一套标准。 从 ASCII、GB2312、GBK 到 GB18030,这些编码方法是向下兼容的,即同一个字符在这些方案中总是有相同的编码,后面的标准支持更多的字符。在这些编码中,英文和中文可以统一地处理。区分中文编码的方法是高字节的最高位不为 0。按照程序员的称呼,GB2312、GBK 到 GB18030 都属于双字节字符集 (DBCS)。三、偏移量计算GB2312收录简化汉字及符号、字母、日文假名等共7445 个图形字符,其中汉字占6763 个。GB2312 规定“对任意一个图形字符都采用两个字节表示,每个字节均采用七位编码表示”,习惯上称第一个字节为“高字节”,即所谓的区码。第二个字节为“低字节”,即所谓的位码。GB2312―80包含了大部分常用的一、二级汉字,和9区的符号。该字符集是几乎所有的中文系统和国际化的软件都支持的中文字符集,这也是最基本的中文字符集。其编码范围是高位0xa1~0xfe,低位也是0xa1~0xfe;汉字从0xb0a1开始,结束于0xf7fe。GB2312将代码表分为94个区,对应第一字节(0xa1~0xfe);每个区94 个位(0xa1~0xfe),对应第二字节。两个字节的值分别为区号值和位号值加32(20H),因此也称为区位码。01~09区为符号、数字区,16~87区为汉字区(0xb0~0xf7),10~15区、88~94区是有待进一步标准化的空白区。GB2312将收录的汉字分成两级:第一级是常用汉字计3755个,置于16~55区,按汉语拼音字母/笔形顺序排列:第二级汉字是次常用汉字计3008 个,置于56~87 区,按部首/笔画顺序排列。故而GB2312 最多能表示6763 个汉字。而GBK内码完全兼容GB2312,同时支持繁体字,总汉字数有2万多个,编码格式如下,每个GBK 码由2 个字节组成,第一个字节为0X81~0XFE,第二个字节分为两部分,一是0X40~0X7E,二是0X80~0XFE。其中与GB2312相同的区域,字完全相同。把第一个字节代表的意义称为区,那么GBK里面总共有126个区(0XFE~0X81+1),每个区内有190 个汉字(0XFE~0X80+0X7E~0X40+2),总共就有126x190=23940 个汉字。点阵库只要按照这个编码规则从0X8140开始,逐一建立,每个区的点阵大小为每个汉字所用的字节数乘以190。这样,就可以得到在这个字库里面定位汉字的方法:当GBKL<0X7F 时:Hp=((GBKH-0x81)×190+GBKL-0X40)×(sizex2);当GBKL>0X80 时:Hp=((GBKH-0x81)×190+GBKL-0X41)×(sizex2);其中GBKH、GBKLL 分别代表GBK 的第一个字节和第二个字节(也就是高位和低位),size 代表汉字字体的大小(比如16 字体,12 字体等),Hp 则为对应汉字点阵数据在字库里面的起始地址。对于GBK 字库和GB2312 字库,他们的解码部分部分略有不同,这个区别主要是由于他们的编码方式不同引起的,对于GBK 字库,解码的方式如下:qh=*code;ql=*(++code);if(ql<0x7f)    ql -= 0x40;else    ql -= 0x41;qh -= 0x81;foffset = ((unsigned long)190*qh + ql)*(size * 2); 对于GB2312 字库,解码的方式如下:qh=*code;ql=*(++code);ql -= 0xa1;qh -= 0xa1;foffset = ((unsigned long)94*qh + ql)*(size * 2);其中qh、ql 分别代表GBK 的第一个字节和第二个字节(也就是高位和低位),size代表汉字字体的大小(比如16字体,12字体等),foffset 则为对应汉字点阵数据在字库里面的起始地址。四、ASCII字符集取模方式需要使用的工具软件:\LCD屏资料\汉字取模工具\PCtoLCD2002\exeASCII字符集:!"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~注意:前面第一个字符是空格。每个字符点阵码所占用的字节数为:(size/8+((size%8)?1:0))*(size/2),其中size:是字库生成时的点阵大小(12/16/24...)PC2LCD2002取模方式设置:阴码+逐列式+顺向+C51格式以下是16*16的字模示例将取出的ASCII字模使用二维数组保存,方便访问。//16*16 ASCII字符集点阵const unsigned char asc2_1608[95][16]={      {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},/*" ",0*/{0x00,0x00,0x00,0x00,0x00,0x00,0x1F,0xCC,0x00,0x0C,0x00,0x00,0x00,0x00,0x00,0x00},/*"!",1*/{0x00,0x00,0x08,0x00,0x30,0x00,0x60,0x00,0x08,0x00,0x30,0x00,0x60,0x00,0x00,0x00},/*""",2*/{0x02,0x20,0x03,0xFC,0x1E,0x20,0x02,0x20,0x03,0xFC,0x1E,0x20,0x02,0x20,0x00,0x00},/*"#",3*/{0x00,0x00,0x0E,0x18,0x11,0x04,0x3F,0xFF,0x10,0x84,0x0C,0x78,0x00,0x00,0x00,0x00},/*"$",4*/{0x0F,0x00,0x10,0x84,0x0F,0x38,0x00,0xC0,0x07,0x78,0x18,0x84,0x00,0x78,0x00,0x00},/*"%",5*/{0x00,0x78,0x0F,0x84,0x10,0xC4,0x11,0x24,0x0E,0x98,0x00,0xE4,0x00,0x84,0x00,0x08},/*"&",6*/{0x08,0x00,0x68,0x00,0x70,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},/*"'",7*/{0x00,0x00,0x00,0x00,0x00,0x00,0x07,0xE0,0x18,0x18,0x20,0x04,0x40,0x02,0x00,0x00},/*"(",8*/{0x00,0x00,0x40,0x02,0x20,0x04,0x18,0x18,0x07,0xE0,0x00,0x00,0x00,0x00,0x00,0x00},/*")",9*/{0x02,0x40,0x02,0x40,0x01,0x80,0x0F,0xF0,0x01,0x80,0x02,0x40,0x02,0x40,0x00,0x00},/*"*",10*/{0x00,0x80,0x00,0x80,0x00,0x80,0x0F,0xF8,0x00,0x80,0x00,0x80,0x00,0x80,0x00,0x00},/*"+",11*/{0x00,0x01,0x00,0x0D,0x00,0x0E,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},/*",",12*/{0x00,0x00,0x00,0x80,0x00,0x80,0x00,0x80,0x00,0x80,0x00,0x80,0x00,0x80,0x00,0x80},/*"-",13*/{0x00,0x00,0x00,0x0C,0x00,0x0C,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},/*".",14*/{0x00,0x00,0x00,0x06,0x00,0x18,0x00,0x60,0x01,0x80,0x06,0x00,0x18,0x00,0x20,0x00},/*"/",15*/{0x00,0x00,0x07,0xF0,0x08,0x08,0x10,0x04,0x10,0x04,0x08,0x08,0x07,0xF0,0x00,0x00},/*"0",16*/{0x00,0x00,0x08,0x04,0x08,0x04,0x1F,0xFC,0x00,0x04,0x00,0x04,0x00,0x00,0x00,0x00},/*"1",17*/{0x00,0x00,0x0E,0x0C,0x10,0x14,0x10,0x24,0x10,0x44,0x11,0x84,0x0E,0x0C,0x00,0x00},/*"2",18*/{0x00,0x00,0x0C,0x18,0x10,0x04,0x11,0x04,0x11,0x04,0x12,0x88,0x0C,0x70,0x00,0x00},/*"3",19*/{0x00,0x00,0x00,0xE0,0x03,0x20,0x04,0x24,0x08,0x24,0x1F,0xFC,0x00,0x24,0x00,0x00},/*"4",20*/{0x00,0x00,0x1F,0x98,0x10,0x84,0x11,0x04,0x11,0x04,0x10,0x88,0x10,0x70,0x00,0x00},/*"5",21*/{0x00,0x00,0x07,0xF0,0x08,0x88,0x11,0x04,0x11,0x04,0x18,0x88,0x00,0x70,0x00,0x00},/*"6",22*/{0x00,0x00,0x1C,0x00,0x10,0x00,0x10,0xFC,0x13,0x00,0x1C,0x00,0x10,0x00,0x00,0x00},/*"7",23*/{0x00,0x00,0x0E,0x38,0x11,0x44,0x10,0x84,0x10,0x84,0x11,0x44,0x0E,0x38,0x00,0x00},/*"8",24*/{0x00,0x00,0x07,0x00,0x08,0x8C,0x10,0x44,0x10,0x44,0x08,0x88,0x07,0xF0,0x00,0x00},/*"9",25*/{0x00,0x00,0x00,0x00,0x00,0x00,0x03,0x0C,0x03,0x0C,0x00,0x00,0x00,0x00,0x00,0x00},/*":",26*/{0x00,0x00,0x00,0x00,0x00,0x01,0x01,0x06,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},/*";",27*/{0x00,0x00,0x00,0x80,0x01,0x40,0x02,0x20,0x04,0x10,0x08,0x08,0x10,0x04,0x00,0x00},/*"<",28*/{0x02,0x20,0x02,0x20,0x02,0x20,0x02,0x20,0x02,0x20,0x02,0x20,0x02,0x20,0x00,0x00},/*"=",29*/{0x00,0x00,0x10,0x04,0x08,0x08,0x04,0x10,0x02,0x20,0x01,0x40,0x00,0x80,0x00,0x00},/*">",30*/{0x00,0x00,0x0E,0x00,0x12,0x00,0x10,0x0C,0x10,0x6C,0x10,0x80,0x0F,0x00,0x00,0x00},/*"?",31*/{0x03,0xE0,0x0C,0x18,0x13,0xE4,0x14,0x24,0x17,0xC4,0x08,0x28,0x07,0xD0,0x00,0x00},/*"@",32*/{0x00,0x04,0x00,0x3C,0x03,0xC4,0x1C,0x40,0x07,0x40,0x00,0xE4,0x00,0x1C,0x00,0x04},/*"A",33*/{0x10,0x04,0x1F,0xFC,0x11,0x04,0x11,0x04,0x11,0x04,0x0E,0x88,0x00,0x70,0x00,0x00},/*"B",34*/{0x03,0xE0,0x0C,0x18,0x10,0x04,0x10,0x04,0x10,0x04,0x10,0x08,0x1C,0x10,0x00,0x00},/*"C",35*/{0x10,0x04,0x1F,0xFC,0x10,0x04,0x10,0x04,0x10,0x04,0x08,0x08,0x07,0xF0,0x00,0x00},/*"D",36*/{0x10,0x04,0x1F,0xFC,0x11,0x04,0x11,0x04,0x17,0xC4,0x10,0x04,0x08,0x18,0x00,0x00},/*"E",37*/{0x10,0x04,0x1F,0xFC,0x11,0x04,0x11,0x00,0x17,0xC0,0x10,0x00,0x08,0x00,0x00,0x00},/*"F",38*/{0x03,0xE0,0x0C,0x18,0x10,0x04,0x10,0x04,0x10,0x44,0x1C,0x78,0x00,0x40,0x00,0x00},/*"G",39*/{0x10,0x04,0x1F,0xFC,0x10,0x84,0x00,0x80,0x00,0x80,0x10,0x84,0x1F,0xFC,0x10,0x04},/*"H",40*/{0x00,0x00,0x10,0x04,0x10,0x04,0x1F,0xFC,0x10,0x04,0x10,0x04,0x00,0x00,0x00,0x00},/*"I",41*/{0x00,0x03,0x00,0x01,0x10,0x01,0x10,0x01,0x1F,0xFE,0x10,0x00,0x10,0x00,0x00,0x00},/*"J",42*/{0x10,0x04,0x1F,0xFC,0x11,0x04,0x03,0x80,0x14,0x64,0x18,0x1C,0x10,0x04,0x00,0x00},/*"K",43*/{0x10,0x04,0x1F,0xFC,0x10,0x04,0x00,0x04,0x00,0x04,0x00,0x04,0x00,0x0C,0x00,0x00},/*"L",44*/{0x10,0x04,0x1F,0xFC,0x1F,0x00,0x00,0xFC,0x1F,0x00,0x1F,0xFC,0x10,0x04,0x00,0x00},/*"M",45*/{0x10,0x04,0x1F,0xFC,0x0C,0x04,0x03,0x00,0x00,0xE0,0x10,0x18,0x1F,0xFC,0x10,0x00},/*"N",46*/{0x07,0xF0,0x08,0x08,0x10,0x04,0x10,0x04,0x10,0x04,0x08,0x08,0x07,0xF0,0x00,0x00},/*"O",47*/{0x10,0x04,0x1F,0xFC,0x10,0x84,0x10,0x80,0x10,0x80,0x10,0x80,0x0F,0x00,0x00,0x00},/*"P",48*/{0x07,0xF0,0x08,0x18,0x10,0x24,0x10,0x24,0x10,0x1C,0x08,0x0A,0x07,0xF2,0x00,0x00},/*"Q",49*/{0x10,0x04,0x1F,0xFC,0x11,0x04,0x11,0x00,0x11,0xC0,0x11,0x30,0x0E,0x0C,0x00,0x04},/*"R",50*/{0x00,0x00,0x0E,0x1C,0x11,0x04,0x10,0x84,0x10,0x84,0x10,0x44,0x1C,0x38,0x00,0x00},/*"S",51*/{0x18,0x00,0x10,0x00,0x10,0x04,0x1F,0xFC,0x10,0x04,0x10,0x00,0x18,0x00,0x00,0x00},/*"T",52*/{0x10,0x00,0x1F,0xF8,0x10,0x04,0x00,0x04,0x00,0x04,0x10,0x04,0x1F,0xF8,0x10,0x00},/*"U",53*/{0x10,0x00,0x1E,0x00,0x11,0xE0,0x00,0x1C,0x00,0x70,0x13,0x80,0x1C,0x00,0x10,0x00},/*"V",54*/{0x1F,0xC0,0x10,0x3C,0x00,0xE0,0x1F,0x00,0x00,0xE0,0x10,0x3C,0x1F,0xC0,0x00,0x00},/*"W",55*/{0x10,0x04,0x18,0x0C,0x16,0x34,0x01,0xC0,0x01,0xC0,0x16,0x34,0x18,0x0C,0x10,0x04},/*"X",56*/{0x10,0x00,0x1C,0x00,0x13,0x04,0x00,0xFC,0x13,0x04,0x1C,0x00,0x10,0x00,0x00,0x00},/*"Y",57*/{0x08,0x04,0x10,0x1C,0x10,0x64,0x10,0x84,0x13,0x04,0x1C,0x04,0x10,0x18,0x00,0x00},/*"Z",58*/{0x00,0x00,0x00,0x00,0x00,0x00,0x7F,0xFE,0x40,0x02,0x40,0x02,0x40,0x02,0x00,0x00},/*"[",59*/{0x00,0x00,0x30,0x00,0x0C,0x00,0x03,0x80,0x00,0x60,0x00,0x1C,0x00,0x03,0x00,0x00},/*"\",60*/{0x00,0x00,0x40,0x02,0x40,0x02,0x40,0x02,0x7F,0xFE,0x00,0x00,0x00,0x00,0x00,0x00},/*"]",61*/{0x00,0x00,0x00,0x00,0x20,0x00,0x40,0x00,0x40,0x00,0x40,0x00,0x20,0x00,0x00,0x00},/*"^",62*/{0x00,0x01,0x00,0x01,0x00,0x01,0x00,0x01,0x00,0x01,0x00,0x01,0x00,0x01,0x00,0x01},/*"_",63*/{0x00,0x00,0x40,0x00,0x40,0x00,0x20,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},/*"`",64*/{0x00,0x00,0x00,0x98,0x01,0x24,0x01,0x44,0x01,0x44,0x01,0x44,0x00,0xFC,0x00,0x04},/*"a",65*/{0x10,0x00,0x1F,0xFC,0x00,0x88,0x01,0x04,0x01,0x04,0x00,0x88,0x00,0x70,0x00,0x00},/*"b",66*/{0x00,0x00,0x00,0x70,0x00,0x88,0x01,0x04,0x01,0x04,0x01,0x04,0x00,0x88,0x00,0x00},/*"c",67*/{0x00,0x00,0x00,0x70,0x00,0x88,0x01,0x04,0x01,0x04,0x11,0x08,0x1F,0xFC,0x00,0x04},/*"d",68*/{0x00,0x00,0x00,0xF8,0x01,0x44,0x01,0x44,0x01,0x44,0x01,0x44,0x00,0xC8,0x00,0x00},/*"e",69*/{0x00,0x00,0x01,0x04,0x01,0x04,0x0F,0xFC,0x11,0x04,0x11,0x04,0x11,0x00,0x18,0x00},/*"f",70*/{0x00,0x00,0x00,0xD6,0x01,0x29,0x01,0x29,0x01,0x29,0x01,0xC9,0x01,0x06,0x00,0x00},/*"g",71*/{0x10,0x04,0x1F,0xFC,0x00,0x84,0x01,0x00,0x01,0x00,0x01,0x04,0x00,0xFC,0x00,0x04},/*"h",72*/{0x00,0x00,0x01,0x04,0x19,0x04,0x19,0xFC,0x00,0x04,0x00,0x04,0x00,0x00,0x00,0x00},/*"i",73*/{0x00,0x00,0x00,0x03,0x00,0x01,0x01,0x01,0x19,0x01,0x19,0xFE,0x00,0x00,0x00,0x00},/*"j",74*/{0x10,0x04,0x1F,0xFC,0x00,0x24,0x00,0x40,0x01,0xB4,0x01,0x0C,0x01,0x04,0x00,0x00},/*"k",75*/{0x00,0x00,0x10,0x04,0x10,0x04,0x1F,0xFC,0x00,0x04,0x00,0x04,0x00,0x00,0x00,0x00},/*"l",76*/{0x01,0x04,0x01,0xFC,0x01,0x04,0x01,0x00,0x01,0xFC,0x01,0x04,0x01,0x00,0x00,0xFC},/*"m",77*/{0x01,0x04,0x01,0xFC,0x00,0x84,0x01,0x00,0x01,0x00,0x01,0x04,0x00,0xFC,0x00,0x04},/*"n",78*/{0x00,0x00,0x00,0xF8,0x01,0x04,0x01,0x04,0x01,0x04,0x01,0x04,0x00,0xF8,0x00,0x00},/*"o",79*/{0x01,0x01,0x01,0xFF,0x00,0x85,0x01,0x04,0x01,0x04,0x00,0x88,0x00,0x70,0x00,0x00},/*"p",80*/{0x00,0x00,0x00,0x70,0x00,0x88,0x01,0x04,0x01,0x04,0x01,0x05,0x01,0xFF,0x00,0x01},/*"q",81*/{0x01,0x04,0x01,0x04,0x01,0xFC,0x00,0x84,0x01,0x04,0x01,0x00,0x01,0x80,0x00,0x00},/*"r",82*/{0x00,0x00,0x00,0xCC,0x01,0x24,0x01,0x24,0x01,0x24,0x01,0x24,0x01,0x98,0x00,0x00},/*"s",83*/{0x00,0x00,0x01,0x00,0x01,0x00,0x07,0xF8,0x01,0x04,0x01,0x04,0x00,0x00,0x00,0x00},/*"t",84*/{0x01,0x00,0x01,0xF8,0x00,0x04,0x00,0x04,0x00,0x04,0x01,0x08,0x01,0xFC,0x00,0x04},/*"u",85*/{0x01,0x00,0x01,0x80,0x01,0x70,0x00,0x0C,0x00,0x10,0x01,0x60,0x01,0x80,0x01,0x00},/*"v",86*/{0x01,0xF0,0x01,0x0C,0x00,0x30,0x01,0xC0,0x00,0x30,0x01,0x0C,0x01,0xF0,0x01,0x00},/*"w",87*/{0x00,0x00,0x01,0x04,0x01,0x8C,0x00,0x74,0x01,0x70,0x01,0x8C,0x01,0x04,0x00,0x00},/*"x",88*/{0x01,0x01,0x01,0x81,0x01,0x71,0x00,0x0E,0x00,0x18,0x01,0x60,0x01,0x80,0x01,0x00},/*"y",89*/{0x00,0x00,0x01,0x84,0x01,0x0C,0x01,0x34,0x01,0x44,0x01,0x84,0x01,0x0C,0x00,0x00},/*"z",90*/{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x00,0x3E,0xFC,0x40,0x02,0x40,0x02},/*"{",91*/{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xFF,0xFF,0x00,0x00,0x00,0x00,0x00,0x00},/*"|",92*/{0x00,0x00,0x40,0x02,0x40,0x02,0x3E,0xFC,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00},/*"}",93*/{0x00,0x00,0x60,0x00,0x80,0x00,0x80,0x00,0x40,0x00,0x40,0x00,0x20,0x00,0x20,0x00},/*"~",94*/};   显示一个字符:void LcdShowChar(u16 x,u16 y,u8 num,u8 size){                                                           u8 temp,t1,t;        u16 y0=y;        u8 csize=size/8+((size%8)?1:0))*(size/2);             //得到字体一个字符对应点阵集所占的字节数           num=num-' ';   //得到偏移后的值(ASCII字库是从空格开始取模,所以-' '就是对应字符的字库)        for(t=0;t<csize;t++)        {                    if(size==12)temp=asc_1206[num][t];            //调用1206字体                 else return;                 for(t1=0;t1<8;t1++)                 {                                                    if(temp&0x80)LcdDrawPoint(x,y,0x0);                         else LcdDrawPoint(x,y,0xFFFF);                         temp<<=1;                         y++;                         if((y-y0)==size)                         {                                  y=y0;                                  x++;                                  break;                         }                 }             }                              } 
  • [交流分享] 聊聊万物互联-Wi-Fi6
    WiFi的历史从802.11的FHFS,DSSS到802.11b的DSSS,到802.11a的OFDM,802.11g的ERP(将OFDM从5G迁移到了2.4G),到802.11n的更宽频带(40MHz)的OFDM技术,到802.11ac的进一步拓宽(80, 80+80,160MHz)的OFDM技术,到802.11ax的更窄的子载波(78.125kHz)的OFDM技术,到讨论中的802.11be的320MHz的OFDM技术。OFDM技术从802.11a,大概2001年的时候,到现在,已经持续演进了20年了,它的抗多径效应,适合无线空间的复杂环境。因为802.11ax(Wi-FI 6)希望覆盖更广的空间,所以把载波宽度进一步变小了。每一代的发展,频带基本是越来越宽,似乎是可以无限的把频宽扩展下去。但是这应该是存在问题的,多样化的需求下,大频宽是可能浪费频段的。毕竟有些地方只需要小的频宽就好了。802.11ax还定义了一个仅支持20MHz的模式,也是瞄准了万物互联的趋势下,小数据,低能耗的搭配。但是802.11be(TB Wi-Fi 7)又把带宽变得更大了,每一代总是希望能更快的。但是单纯更快有什么意思呢。OFDM不止可以分频带,因为网络的特性,还可以分用户,这是Wi-Fi现在越来越看重的,希望越来越多的用户都是使用这个网络,而且还要能用。从802.11ac(2013)开始的Wi-Fi5已经引入了多用户的观念,对应的技术是MU-MIMO。它是这么样的,不同的用户用的是不同的天线。到了802.1ax(Wi-Fi6),支持上行的MU-MIMO,而且还把一个频段同时分配给了多个用户(OFDMA),每个用户最少可以只用2MHz的带宽。一下还把上行和下行都给加进去了。不止如此,还加入了BSS Coloring技术,弱化Exposed Node的问题,提升密集网络覆盖下的并行性。Wi-Fi6有这么多优势,其实它的主要着笔点在于密集用户,密集网络。想想现在Wi-Fi6的设备已经都出来1两年了,但是也并没有多改变生态。其实现在大家也觉得够用了,就像我自己,这种Wi-Fi6的理念对人这个用户来讲真的是有点超前了,哪里去找如此密集的人流和网络?办公场所,体育场?这些场合毕竟是少数。自动化生产车间也许是比较合适的,不过不是人流,而是物流。Wi-Fi7似乎要在这个道路上越走越远,越来越远…