2020/11/1
读博警告:
本人纯属小白一枚,刚开始接触学习STM32。本篇微博只是想记录自己在做项目中过程中遇到的问题以及最后的解决办法。第一次更博,语言表达能力不好还很乱,望见谅。如果有表述不对的地方,大家可以指正出来,欢迎大家一起学习交流!
文章目录
项目描述
要求:
1、监测设备一天的用电量。
2、统计开机、关机时间,计算使用率。
3、有可能的话实现手机端实时监控
预计方案:
STM32单片机通过RS-485串口驱动电力表模块采集数据,然后再通过TTL串口将数据发送给ZigBee终端。ZigBee终端在接收到数据后通过无线传输将数据发送给协调器,ZigBee协调器通过RS232串口通讯将数据发送给上位机。上位机实时显示数据。另外,还可以开发一款手机APP,上位机将数据保存,发送到手机端,通过手机端对数据进行实时监控。
其中,电力表模块、STM32模块、ZigBee终端模块这三部分组成一个数据采集模块,放置在机器人上,用于采集数据。ZigBee协调器模块和上位机构成显示模块,用于显示耗电量、使用效率等数据。
项目系统结构图如下:
项目实施计划
通信
单片机与电力表之间通过串口1通讯,上位机与单片机之间通过串口2通讯 。
串口1、2的初始化程序、中端配置程序如下:
//串口1初始化函数
void usart1_init(u32 bound)
{
//GPIO端口设置
GPIO_InitTypeDef GPIO_InitStructure;//创建GPIO的初始化结构体
USART_InitTypeDef USART_InitStructure;//创建USART的初始化结构体
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1|RCC_APB2Periph_GPIOA, ENABLE); //使能USART1,GPIOA时钟
//USART1_TX GPIOA.9
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; //PA.9 RX
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽输出
GPIO_Init(GPIOA, &GPIO_InitStructure);//初始化GPIOA.9
//USART1_RX GPIOA.10初始化
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;//PA10
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;//浮空输入
GPIO_Init(GPIOA, &GPIO_InitStructure);//初始化GPIOA.10
//USART 初始化设置
USART_InitStructure.USART_BaudRate = bound;//串口波特率
USART_InitStructure.USART_WordLength = USART_WordLength_8b;//字长为8位数据格式
USART_InitStructure.USART_StopBits = USART_StopBits_1;//一个停止位
USART_InitStructure.USART_Parity = USART_Parity_No;//无奇偶校验位
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;//无硬件数据流控制
USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; //收发模式
USART_Init(USART1, &USART_InitStructure); //初始化串口1
USART_Cmd(USART1, ENABLE); //使能串口1
}
//串口1中断配置
void usart1_NVIC__init(void)
{
NVIC_InitTypeDef NVIC_InitStructure;//创建NVIC的初始化结构体
//Usart1 NVIC 配置
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=3 ;//抢占优先级3
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3; //子优先级3
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ通道使能
NVIC_Init(&NVIC_InitStructure); //根据指定的参数初始化VIC寄存器
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);//开启串口接受中断
}
void usart2_init(u32 bound)
{
//GPIO端口设置
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);//使能GPIOA时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2,ENABLE);//使能USART2时钟
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2; //PA2 tx
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3;//PA3 Rx
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; //浮空输入
GPIO_Init(GPIOA, &GPIO_InitStructure);
//USART2配置
USART_InitStructure.USART_BaudRate = bound;//波特率设置
USART_InitStructure.USART_WordLength = USART_WordLength_8b;//8位数据长度
USART_InitStructure.USART_StopBits = USART_StopBits_1;//一个停止位
USART_InitStructure.USART_Parity = USART_Parity_No;///奇偶校验位
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;//无硬件数据流控制
USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;//收发模式
USART_Init(USART2, &USART_InitStructure); ; //初始化串口
USART_Cmd(USART2, ENABLE); //使能串口
}
//串口2中断配置
void usart2_NVIC__init(void)
{
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = USART2_IRQn; //使能串口2中断
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2; //先占优先级2级
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3; //从优先级2级
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //使能外部中断通道
NVIC_Init(&NVIC_InitStructure); //根据NVIC_InitStruct中指定的参数初始化外设NVIC寄存器
USART_ITConfig(USART2, USART_IT_RXNE, ENABLE);//开启中?
}
上位机与单片机之间
上位机与单片机之间通过串口2通讯 。当上位机向单片机发送命令时,单片机接收到命令执行相应操作。比如上位机发送“ I”,单片机就会向电力表发送相应的查电流的代码。
串口2中断服务程序如下:
此处千万要注意,千万要注意中断服务程序的名字不能写错,否则就不能进中断。(当时这个Bug找了好久…想哭 悲伤 哭泣 委屈)
//串口2中断服务函数
void USART2_IRQHandler(void)
{
if(USART_GetITStatus(USART2, USART_IT_RXNE) == SET)//检测中断接收标志位是否置1
uctemp=USART_ReceiveData(USART2);
}
电力表与单片机之间
单片机——>电力表
MODBUS_RTU通讯协议补充
电力表提供了RS485通讯接口,采用MODBUS——RTU通讯规约。
例如:主机发送数据帧:读三相电流值
地址 | 命令 | 起始地址(高位在前) | 寄存器数(低位在前) | 校验码 |
---|---|---|---|---|
01H | 03H | 00H,45H | 00H,06H | D4H,1DH |
存电流寄存器的起始地址是“00H,45H”(查说明书MODBUS——RTU地址信息表得,即69),存放三相电流的寄存器一共有六个,所以寄存器数是“00H,06H”。
校验码是通过专用计算器算的。
所以定义读取电压、电流、功率的数组如下:
u8 current[8]={0x01,0x03,0x00,0x45,0x00,0x06,0xD4,0x1D};
u8 voltage[8]={0x01,0x03,0x00,0x39,0x00,0x06,0x15,0xC5};
u8 power[8]={0x01,0x03,0x00,0x4B,0x00,0x06,0xB5,0xDE};
串口发送指令补充
//发送一个字节
void sendByte(USART_TypeDef* USARTx,u8 date)
{
USART_SendData(USARTx, date);
while(USART_GetFlagStatus(USARTx, USART_FLAG_TXE)==RESET);
}
//发送两个字节
void sendTwoByte(USART_TypeDef* USARTx,u16 date)
{
u8 temp_h,temp_l;
temp_h =(date&0xff00)>>8;
temp_l=(date&0xff);
sendByte(USARTx,temp_h);
sendByte(USARTx,temp_l);
}
//发送四个字节
void sendFourByte(USART_TypeDef* USARTx,u32 date)
{
u8 temp1,temp2,temp3,temp4;
temp1 =(date&0xff000000)>>24;
temp2=(date&0xff0000)>>16;
temp3=(date&0xff00)>>8;
temp4=(date&0xff);
sendByte(USARTx,temp1);
sendByte(USARTx,temp2);
sendByte(USARTx,temp3);
sendByte(USARTx,temp4);
}
//发送8位数组
void sendArry(USART_TypeDef* USARTx,u8 *arry,u8 num)
{
u8 i;
for(i=0;i<num;i++)
{
sendByte(USARTx,arry[i]);
}
while(USART_GetFlagStatus(USARTx, USART_FLAG_TC)==RESET);
}
//发送字符串
void Usart_Sendstr(USART_TypeDef* USARTx,char*str)
{
u8 i=0;
do
{
sendByte(USARTx,str[i]);
i++;
}
while(*(str+i)!='\n');
while(USART_GetFlagStatus(USARTx, USART_FLAG_TC)==RESET);
}
单片机接收到命令后,通过串口1向电力表发送相应指令,电力表通过串口1返回数据
例如:
其中数据段一共有12个字节,没4个字节表示一相的电流值。
串口1接收到数据后进入串口1的中断服务子程序,把接收到的数据存在接收缓存寄存器里面,具体程序如下:
//串口1中断服务函数
//USART_RX_BUF[17];接收缓冲,最大17个字节 。 USART_RX_STA;接收状态标记,记录接收到第几个字节
void USART1_IRQHandler(void)
{
u8 Res;
if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) //接收中断
{
Res =USART_ReceiveData(USART1); //读取接收到的数据
USART_RX_BUF[USART_RX_STA] = Res;
USART_RX_STA++;
if(USART_RX_STA==17)//返回的数据一共有17个字节
{
USART_RX_STA=0;
flag =1;//flag是接收完成标志位
}
}
}
单片机再把返回的数据发送给上位机。
if(flag)//串口1接收中断完成
{
for(t=3;t<15;t++)//数组里的第四个字节开始是电流值
{
USART_SendData(USART2, USART_RX_BUF[t]);//向串口2发送数据
while(USART_GetFlagStatus(USART2,USART_FLAG_TC)!=SET);//等待发送结束
}
flag=0;
}
只接A相负载的实验结果如图
可见A相的电流值为“32 30 20 C5”,这是一个4个字节表示的浮点型数据,标准的IEEE-754标准,后续还要对数据进行处理,直接用ASCLL码表示浮点型。
未完待续。。。。
2020/11/11
双十一剁完手就要好好工作啦,努力学习,天天向上哈!!
数据处理
上次已经通过串口读到电表采集的电流值了,但是它是用4个字节的16进制数表示的,不方便读取,所以要根据IEEE-754标准将4个字节的16进制数转换成10进制浮点数。
IEEE-754标准16进制(32位)转化成10进制float
关于IEEE-754标准的只是大家可以参见这篇文章:IEEE754 浮点数的表示方法
1、首先返回来的一相数据是“32 30 20 C5”,它占4个字节存储在4个内存单元中,目前还不是一个32位的数,所以要先把它转换成一下存在一个内存单元里 ,故需要定义一个变量temp,具体程序如下:
u32 temp1=0,temp2=0,temp3=0;//用于存放每相转化后的32位16进制数
temp1=temp1|(USART_RX_BUF[3]<<24);//USART_RX_BUF[3]变为temp1的高8位(25~32位)
temp1=temp1|(USART_RX_BUF[4]<<16);
temp1=temp1|(USART_RX_BUF[5]<<8);
temp1=temp1|(USART_RX_BUF[6]);
USART_RX_BUF[]是一个含有17个元素的全局变量数组,用来存放电表返回的数据。从第4个元素开始是数据值,故需要从USART_RX_BUF[3]开始取值。
extern u8 USART_RX_BUF[17];
!!!注意全局变量不能重复定义,只定义在一个.h文件中就可以了。
2、现在16进制的32位数已经保存在了temp1这个内存单元中,下面要把temp1内存单元的数按照IEEE-754标准转化成10进制浮点型。
16进制(32位)转化成10进制float程序如下:
//============================================================
//函数名称 :Float32
//函数功能 :将32位的float转换为10进制数,IEEE 754标准
//输入变量 :NumFloat32:待转换32位浮点数
//返 回 值 :计算结果 无符号整形 u32 ,返回-1为负数或者超限;
//============================================================
float Float32(u32 NumFloat32)
{
signed char sbit, ebits;// sbit符号位,ebits指数位
signed int mbits,temp,i;//mbits位数位
float Float32_sum;
float Float32_result =0;
temp = 0x00400000;
Float32_sum = 1.0;
i = 1;
sbit = NumFloat32 >> 31;
ebits = (NumFloat32 >> 23) & 0xff;
if (sbit) //若需要负数部分,sbit为1时,result*-1,去掉判断;
{
return -1;
}
ebits -= 127; //得到阶码<0,即结果小于1,返回0;
if (ebits < 0)//负数
{
return 0;
}
while (temp != 0)
{
mbits = NumFloat32 & temp;
if (mbits!=0)
Float32_sum += pow(2, -i);//pow是指数运算函数,2是底数,-i是指数
temp = temp >> 1;
i++;
}
Float32_result =Float32_sum* pow(2, ebits);
return Float32_result;
}
3、后来师兄说利用共用体就可以实现16进制(32位)转化成10进制float的功能,不用调用复杂的Float32函数。
共用体可以使不同的数据类型来共享相同的地址空间。首先定义一个共用体,既可以存放浮点型又可以存放整型。程序如下:
union data
{
float dataInFloat;//存放浮点型数
u32 dataInUnsingedInt;//存放整数
};
定义共用体变量:
union data currentData;//定义一个共用体变量。
将32位的16进制数(整型)存放在共用体中,然后调用的时候以浮点型取出。这样它就可以实现16进制(32位)转化成10进制float的功能。
currentData.dataInUnsingedInt = temp1;
Turn_Float_to_str(currentData.dataInFloat);
float浮点型数据转化成char字符型数据输出
通常我们发送数据的模式有两种,一种16进制,一种是ASCII码。16进制的经常会用来和仪器PLC等设备通讯。ACSII码是一种文本模式。当我们不点选16进制时,按文本模式发送。这时我们输入的文本区的内容是一个个字符。比如输入50 ,这时50为‘5’和‘0’两个字符。发送的时候会将字符‘5’的ASCII码和字符‘0’的ASCII码发送出去,即是0x35,0x30 。当我们按16进制发送50 时,这时50为一个数即0x50。
如果要通过串口调试助手显示浮点型数据,就要把浮点型数据每一位对应的ASCLL码发送给串口。
具体程序如下:
//============================================================
//函数名称 :Turn_Float_to_str
//函数功能 :将浮点型数据转换成字符串数组输出给串口调试助手
//输入变量 :要转化的数据date 。
//返 回 值 :字符串的数组str[]
//============================================================
void Turn_Float_to_str(float date)
{
u8 len1,len2,i;//len1是整数部分的位数,len2是小数部分的位数
//【!】字符串类型的数据定义的时候需要初始化,发送完成之后需要复位,因为
u8 str1[5],str2[5],str[10];//str1[],str2[5]分别存放整数部分 小数部分的每一位数字(肯定是从个位开始倒序存的)str[10]把他们正过来再添加小数点
u16 zhengshu,xiaoshu;
u16 a1;//a1做中间变量,防止提取整数部分时zhengshu的值改变 影响下面xiaoshu计算,
//test by zzz 20201101【!】
//经测试,若使用强制数据类型转换在某些情况下会出现数据异常,但是问题并没能复现,所以将代码改了回来,如果后续测试出现问题可以考虑修改此处,下面小数部分同
zhengshu=(u16)date;//把浮点数的整数部分提取出来
a1=zhengshu;
len1=number_figure(zhengshu);//调用函数number_figure()计算整数部分的长度
//把整数部分每位提取出来
//如果整数部分是零
if(len1==0)
{
str[0]=0x30;
len1=1;
}
else
{
for(i=0;i<len1;i++)
{
str1[i]=(u8)(a1%10);
a1 /=10;
str[len1-1-i]=str1[i]+0x30;//十进制数与其对应的字符他们的ASCLL码相差0x30
}
}
xiaoshu=(u16)((date-zhengshu)*100);//保留两位小数
len2=number_figure(xiaoshu);
for(i=0;i<len2;i++)
{
str2[i]=(u8)(xiaoshu%10);
xiaoshu /=10;
str[len1+len2-i]=str2[i]+0x30;
}
str[len1]=0x2E;
sendArry(USART2,str,(len1+len2+1));
a1 = 0;
}
这样就可以将浮点型数转化成字符串通过串口输出。
附上成果图:
至此,第一阶段的任务基本完成,下面就该写上位机程序了。先暂时拜拜了,我去学上位机了!!!