爆肝9万字,我已从小白晋升ARM嵌入式工程师!带你从零熟悉常用的M4嵌入式功能,建议收藏(含码源)(6)

简介: 通讯协议又称通信规程,是指通信双方对数据传送控制的一种约定。约定中包括对数据格式,同步方式,传送速度,传送步骤,检纠错方式以及控制字符定义等问题做出统一规定,通信双方必须共同遵守。

9.6 一主多从+自定义协议

9.6.1 搭建自定义协议

 通讯协议又称通信规程,是指通信双方对数据传送控制的一种约定。约定中包括对数据格式,同步方式,传送速度,传送步骤,检纠错方式以及控制字符定义等问题做出统一规定,通信双方必须共同遵守。

搭建自定义协议的话,那么自己写的程序,就根据自己制度的要求来

9.6.2 常见的协议规则

帧数据(数据包):

2345_image_file_copy_112.jpg

常见的数据包结构

  • 帧头:一帧数据开始,可以使用多个字节。 假如以一个字节:0XFF  ----自己定义。
  • 地址信息:跟哪个设备进行通信(类型于IIC器件地址) ---0X01  0X02  芯片ID;比如我们现在是一主多从,那么这个地址信息可以用于表示从机的ID编号
  • 数据类型:数据类型:如 0x01 –代表发的是字符。 0x02 –代表16进制   ---- 类似命令;
  • 数据长度:数据长度: ---  如果发送的数据大于256个字节,至少用2个字节表示  0x00 0x05;
  • 数据块:数据,发送的数据;
  • 检验码:求和,或者适应CRC16/CRC8等校验算法;
  • 帧尾:一帧数据结束,可以使用多个字节。 假如以一个字节:0XFE  ----自己定义。

定义从机协议结构体:

2345_image_file_copy_113.jpg

9.6.3 RS485自定义协议+1主多从通信-按键控制-单次数据通信程序

主机:

#include "rs485.h"                  // Device header
#define RE_DE PGout(8)   //发送/接收器共用一个引脚
void RS485_Init(int bps)  
{
  GPIO_InitTypeDef u2_TXRX,RE;
  USART_InitTypeDef u2;
  NVIC_InitTypeDef u2_NVIC;
  RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA|RCC_AHB1Periph_GPIOG,ENABLE);
  RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2, ENABLE);
  GPIO_PinAFConfig(GPIOA,GPIO_PinSource2,GPIO_AF_USART2);
  GPIO_PinAFConfig(GPIOA,GPIO_PinSource3,GPIO_AF_USART2);
  //PG8-发送/接收器使能
  RE.GPIO_Mode =GPIO_Mode_OUT;
  RE.GPIO_OType = GPIO_OType_PP;
  RE.GPIO_Pin = GPIO_Pin_8;
  RE.GPIO_PuPd = GPIO_PuPd_DOWN;   //默认下拉
  RE.GPIO_Speed = GPIO_High_Speed;
  GPIO_Init(GPIOG,&RE);   
  //PA9-发送
  u2_TXRX.GPIO_Mode =GPIO_Mode_AF;
  u2_TXRX.GPIO_OType = GPIO_OType_PP;
  u2_TXRX.GPIO_Pin = GPIO_Pin_2;
  u2_TXRX.GPIO_PuPd = GPIO_PuPd_UP;   //默认下拉
  u2_TXRX.GPIO_Speed = GPIO_High_Speed;
  GPIO_Init(GPIOA,&u2_TXRX);   //根据u2_TXRX配置的参数进行初始化
  //PA10-接收
  u2_TXRX.GPIO_Mode =GPIO_Mode_AF;
  u2_TXRX.GPIO_OType = GPIO_OType_PP;
  u2_TXRX.GPIO_Pin = GPIO_Pin_3;
  u2_TXRX.GPIO_PuPd = GPIO_PuPd_NOPULL;   //默认下拉
  u2_TXRX.GPIO_Speed = GPIO_High_Speed;
  GPIO_Init(GPIOA,&u2_TXRX);
  //USART
  u2.USART_BaudRate=bps;   //波特率
  u2.USART_HardwareFlowControl=USART_HardwareFlowControl_None;  //无硬件流
  u2.USART_Mode=USART_Mode_Tx|USART_Mode_Rx;  //发送和接收
  u2.USART_Parity=USART_Parity_No;      //无校验
  u2.USART_StopBits=USART_StopBits_1;     //停止位
  u2.USART_WordLength=USART_WordLength_8b;  //数据位
  USART_Init(USART2, &u2);   //根据u2配置的参数进行初始化
  //记得分开写中断
  USART_ITConfig(USART2, USART_IT_RXNE,ENABLE);   //使能串口接收中断
  USART_ITConfig(USART2, USART_IT_IDLE,ENABLE);   //使能串口空闲中断 
  u2_NVIC.NVIC_IRQChannel = USART2_IRQn;
  u2_NVIC.NVIC_IRQChannelPreemptionPriority = 1;  //抢占为1
  u2_NVIC.NVIC_IRQChannelSubPriority = 1;//响应为0
  u2_NVIC.NVIC_IRQChannelCmd = ENABLE;
  NVIC_Init(&u2_NVIC);  
  USART_Cmd(USART2, ENABLE);  //使能串口进行工作
}
/********************************************************************
函数功能:串口1发送1个字节的字符数据
参数1:data:你要发送的数据
参数2:无
返回值:无
作者:xxx
********************************************************************/
void RS485_sendbyte(char data)
{
  RE_DE=1;    //使能485发送
  while(!(USART_GetFlagStatus(USART2,USART_FLAG_TC))); //等待判断发送寄存器为空
  USART_SendData(USART2,data);
  while(!(USART_GetFlagStatus(USART2,USART_FLAG_TC))); //等待为空,说明发送完毕
  RE_DE=0;    //关闭发送,默认接收
}
/********************************************************************
函数功能:串口1发送一串字符数据
参数1:data:你要发送的数据
参数2:无
返回值:无
作者:xxx
********************************************************************/
void RS485_sendstring(char *data)
{
  RE_DE=1;    //使能485发送
  while(*data != '\0')//循环发送,直到遇到\0,停止发送
  {
    while(!(USART_GetFlagStatus(USART2,USART_FLAG_TC))); //等待判断发送寄存器为空
    USART_SendData(USART2,*data++);
  }
  while(!(USART_GetFlagStatus(USART2,USART_FLAG_TC)));  //等待字符串最后一个数据发送完毕
  RE_DE=0;    //关闭发送,默认接收
}
void RS485_SendLenString(char *data,u16 len)
{
  RE_DE=1;    //使能485发送
  while(len)//循环发送,直到遇到\0,停止发送
  {
    while(!(USART_GetFlagStatus(USART2,USART_FLAG_TC))); //等待判断发送寄存器为空
    USART_SendData(USART2,*data++);
    len--;
  }
  while(!(USART_GetFlagStatus(USART2,USART_FLAG_TC)));  //等待字符串最后一个数据发送完毕
  RE_DE=0;    //关闭发送,默认接收
}
//接收中断+空闲中断
struct U2_DATA  rs485={0,0,0};  //定义结构体变量,同时初始化为0
//接收中断:每收到一个字符数据就会执行一次USART2_IRQHandler中断服务函数
//空闲中断:收完数据之后,串口产生空闲,会自动执行一次USART2_IRQHandler中断服务函数
void USART2_IRQHandler(void)
{
  if(USART_GetITStatus(USART2, USART_IT_RXNE)==SET)   //接收中断,存储数据
  {
    rs485.buf[rs485.len]=USART_ReceiveData(USART2);  //读数据并清除中断标志位
    rs485.len++;  //自增,为下个数据存储做准备
  }else if(USART_GetITStatus(USART2, USART_IT_IDLE)==SET)  //空闲中断,接收数据结束
  {
    USART_ClearITPendingBit(USART2, USART_IT_IDLE);  //读SR
    USART_ReceiveData(USART2);   //读DR     
    rs485.ok_flag=1;          //接收完成
    rs485.buf[rs485.len]='\0';  //添加结束符   
  }
}
//Slave s1={0xfd,1,0,0,{0},0,0xfe};   //定义结构体,并初始化
Master M1={0xfd,0,0,0xfe};
//发送数据包
void Master_SendDataPackage(u8 id,u8 cmd)
{
  M1.id=id;     //呼叫从机1
  M1.cmd=cmd; //上传温度、光照、有毒气体
  //void RS485_SendLenString(char *data,u16 len)
  RS485_sendbyte(M1.head);   //帧头
  RS485_sendbyte(M1.id);     //从机id=0x2c 
  RS485_sendbyte(M1.cmd);    //命令
  RS485_sendbyte(M1.end);    //帧尾
}

main.c

#include "main.h" 
//extern const unsigned char gImage_image[307200];
XPT2046 xy;
RTC_TimeTypeDef time;   //时间
RTC_DateTypeDef date;   //日期
int main(void)
{
  int i;
  u8 M1_check=0;
  float wendu=0,shidu=0;
  u8 key_number=0;
  u8 buf[2];
  char rtc_buf[64];
  NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //NVIC 组2,抢2占2
  SysTick_init();  //系统滴答延时
  LED_Init();    //LED
  beep_Init();   //蜂鸣器
  KEY_Init();
  //KEY_Exti_Init();  //按键外部中断
  USART1_Init(115200);   //串口初始化115200
  //LED1_PWM_Init(8400,100) ;  //168分频8400=21000hz=2.1khz  100为比较最大值
  //LED2_PWM_Init(8400,100);    //168分频8400=21000hz=2.1khz  100为比较最大值
  W25Q64_SPI1_Init();   //w25q64初始化
  W25Q64_ReadID(buf);   //读ID
  printf("W25Q64 ID:0x%x  0x%x\r\n",buf[0],buf[1]);
  LCD_ILI9486_Init();  //LCD初始化
  Touch_Init();   //触摸屏初始化
  delay_ms(1000);
  touch_adjust();   //触摸屏校准
  printf("校准成功\r\n"); 
  RTC_Time_Init(22,8,12,11,18,50,5);
  //WWDG_Init(127,125);  //窗口值127,计数器值为125,即初始化值,也是喂狗值,不能大于窗口值127,否则也会触发复位
  RS485_Init(115200);
  //TIM4_Init(8400,9000);    //定时0.9S,进行喂狗
  //IWDG_Init(IWDG_Prescaler_32,1000);  //1S之内要喂狗,预分频为32,32Khz/32=1khz,1S=1khz,
                    //重装载值1个数为1ms,写入1000表示1000ms内要喂狗,否则会产生复位
//  for(i=0;i<15;i++) 
//  {
//    delay_ms(1000);
//    if(FONT_FLAG==1) break;
//  }
//  
//  if(FONT_FLAG==1 && i < 10)  //开机10S内按下按键,触发下载字库操作,执行下载功能
//  {
//    W25Q64_Write_GB2312_ASCII();    //下载字库
//  }
  //LCD_DrawRectangle(10, 150,60,200,RED);   //显示一个矩形
  while (1)
    {
    key_number=key_scan();
    if(key_number==1)
    {
      Master_SendDataPackage(1,0xff);   
      printf("呼叫从机1\r\n");
    }else if(key_number==2)
    {
      Master_SendDataPackage(2,0xff);   
      printf("呼叫从机2\r\n");
    }
    //主机使用触摸屏控制,选择采集对应的从机数据并显示
    if(rs485.ok_flag==1)  //收到从机返回数据
    {
      printf("主机收到从机%d返回的数据\r\n",rs485.buf[1]);
      if(rs485.buf[0] == M1.head  && rs485.buf[rs485.len-1] == M1.end)  //帧头和帧尾正确
      {
        M1_check=0;    //校验码清0
        for(i=3;i<15;i++)
        {
          M1_check+=rs485.buf[i];
          //printf("主机下标%d值=%d\r\n",i,rs485.buf[i]);   //收到的数据
          //printf("计算和值=%d\r\n",M1_check);   //收到的数据
        }
        //printf("主机计算出来的校验码:%d\r\n",M1_check);
        //printf("从机校验码:%d\r\n",rs485.buf[rs485.len-2]);
        if(M1_check==rs485.buf[rs485.len-2])
        {
          printf("校验ok\r\n");
          shidu=rs485.buf[3]+(rs485.buf[4]/100.0);          //56.000000
          wendu=rs485.buf[5]+(rs485.buf[6]/100.0);          //28.560000
          printf("从机%d  温度:%.2f  湿度:%.2f\r\n",rs485.buf[1],wendu,shidu);
        }
      }
      rs485.ok_flag=0;
      rs485.len=0;
      memset(rs485.buf,0,256);  //清0
    }
  /*  
    delay_ms(900);
    RTC_GetTime(RTC_Format_BIN, &time);
    RTC_GetDate(RTC_Format_BIN, &date);
    printf("%d年%d月%d日 %d时 %d分 %d秒\r\n",date.RTC_Year,date.RTC_Month,date.RTC_Date,time.RTC_Hours,time.RTC_Minutes,time.RTC_Seconds);
    sprintf(rtc_buf,"20%d年%d月%d日 %02d时%02d分%02d秒",date.RTC_Year,date.RTC_Month,date.RTC_Date,time.RTC_Hours,time.RTC_Minutes,time.RTC_Seconds);
    LCD_Show_GB2312_ASCII(100,20,WHITE,RED,rtc_buf);
    if(T_PEN==0)
    {       
      get_xpt2046_adjust_xyval(&xy);   //获取校准后的坐标值
      if((xy.xval >=10 && xy.xval<=60) && (xy.yval >=150 && xy.yval<=200))
      {
        printf("按下界面按钮\r\n");
        RTC_Set_time(15,30,52);    //修改时间
      }
      printf("x轴:%d y轴:%d\r\n",xy.xval,xy.yval);        
      while(!T_PEN)delay_ms(10);
    }
*/
/*
    for(i=0;i<100;i++) 
    {
      delay_ms(10);
      TIM_SetCompare1(TIM10,i); //修改占空比,低电平占空比不断增加,越来越亮
      TIM_SetCompare1(TIM14,i); //修改占空比,低电平占空比不断增加,越来越亮
    }
    for(i=100;i>=0;i--)  
    {
      delay_ms(10);
      TIM_SetCompare1(TIM10,i); //修改占空比,低电平占空比不断减少,越来越暗
      TIM_SetCompare1(TIM14,i); //修改占空比,低电平占空比不断增加,越来越亮
    }
*/    
  /*  
    if(T_PEN==0){
      get_xpt2046_adjust_xyval(&xy);   //获取校准后的坐标值
      if((xy.xval >=10 && xy.xval<=60) && (xy.yval >=150 && xy.yval<=200))
      {
        printf("按下界面按钮\r\n");
      }
      printf("x轴:%d y轴:%d\r\n",xy.xval,xy.yval);        
      while(!T_PEN)delay_ms(10);
    }
  */
    //触摸屏+LCD作业:
//    if(u1.ok_flag==1)  //说明串口接收完数据
//    {
//      printf("%s\r\n",u1.buf);  //发送一串数据到电脑回显
//      //清0
//      u1.ok_flag=0;
//      u1.len=0;
//      memset(u1.buf,0,sizeof(u1.buf));  
//    } 
/*    
    for(i=0;i<100;i++) 
    {
      delay_ms(10);
      TIM_SetCompare1(TIM10,i); //修改占空比,低电平占空比不断增加,越来越亮
      TIM_SetCompare1(TIM14,i); //修改占空比,低电平占空比不断增加,越来越亮
    }
    for(i=100;i>=0;i--) 
    {
      delay_ms(10);
      TIM_SetCompare1(TIM10,i); //修改占空比,低电平占空比不断减少,越来越暗
      TIM_SetCompare1(TIM14,i); //修改占空比,低电平占空比不断增加,越来越亮
    }
*/    
    }
}

从机

485.c

#include "rs485.h"                  // Device header
#include "delay.h" 
#define RE_DE PAout(12)
void RS485_Init(int bps)  
{
  GPIO_InitTypeDef U3_TXRX,re;
  USART_InitTypeDef U3;
  NVIC_InitTypeDef NVIC_U3;
  RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART3, ENABLE);  
  RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_GPIOB, ENABLE); //RCC
  re.GPIO_Mode=GPIO_Mode_Out_PP;    //复用推挽输出模式       GPIO
  re.GPIO_Pin=GPIO_Pin_12;      //引脚9
  re.GPIO_Speed=GPIO_Speed_50MHz;   //最高速50Mhz
  GPIO_Init(GPIOA, &re);        //根据U1_TXRX配置的参数进行初始化
  //PB10发送
  U3_TXRX.GPIO_Mode=GPIO_Mode_AF_PP;    //复用推挽输出模式       GPIO
  U3_TXRX.GPIO_Pin=GPIO_Pin_10;     //引脚9
  U3_TXRX.GPIO_Speed=GPIO_Speed_50MHz;  //最高速50Mhz
  GPIO_Init(GPIOB, &U3_TXRX);       //根据U1_TXRX配置的参数进行初始化
  //PB11-接收
  U3_TXRX.GPIO_Mode=GPIO_Mode_IPU;      //上拉输入模式
  U3_TXRX.GPIO_Pin=GPIO_Pin_11;     //引脚10
  U3_TXRX.GPIO_Speed=GPIO_Speed_50MHz;  //最高速50Mhz
  GPIO_Init(GPIOB, &U3_TXRX);       //根据U1_TXRX配置的参数进行初始化
  //USART
  U3.USART_BaudRate=bps;   //波特率
  U3.USART_HardwareFlowControl=USART_HardwareFlowControl_None;  //无硬件流
  U3.USART_Mode=USART_Mode_Tx|USART_Mode_Rx;  //发送和接收
  U3.USART_Parity=USART_Parity_No;      //无校验
  U3.USART_StopBits=USART_StopBits_1;     //停止位
  U3.USART_WordLength=USART_WordLength_8b;  //数据位
  USART_Init(USART3, &U3);   //根据U1配置的参数进行初始化
  NVIC_U3.NVIC_IRQChannel=USART3_IRQn;   //设置的NVIC优先级的中断编号
  NVIC_U3.NVIC_IRQChannelCmd=ENABLE;
  NVIC_U3.NVIC_IRQChannelPreemptionPriority=2;  //抢占优先级
  NVIC_U3.NVIC_IRQChannelSubPriority=0;     //响应优先级
  NVIC_Init(&NVIC_U3);
  //记得分开写中断
  USART_ITConfig(USART3, USART_IT_RXNE,ENABLE);   //使能串口接收中断
  USART_ITConfig(USART3, USART_IT_IDLE,ENABLE);   //使能串口空闲中断 
  //NVIC_EnableIRQ(37);     //使能RS485中断编号 
  USART_Cmd(USART3, ENABLE);  //使能串口进行工作
  RE_DE=0;  //默认接收数据
}
/********************************************************************
函数功能:串口1发送1个字节的字符数据
参数1:data:你要发送的数据
参数2:无
返回值:无
作者:xxx
********************************************************************/
void RS485_sendbyte(u8 data)
{
  RE_DE=1;//使能485发送
  while(!(USART_GetFlagStatus(USART3,USART_FLAG_TC))); //等待判断发送寄存器为空
  USART_SendData(USART3,data);
  while(!(USART_GetFlagStatus(USART3,USART_FLAG_TC))); //等待为空,发送完成
  RE_DE=0; //关闭发送,默认接收
}
/********************************************************************
函数功能:串口1发送一串字符数据
参数1:data:你要发送的数据
参数2:无
返回值:无
作者:xxx
********************************************************************/
void RS485_sendstring(u8 *data)
{
  RE_DE=1;    //使能485发送
  while(*data != '\0')//循环发送,直到遇到\0,停止发送
  {
    while(!(USART_GetFlagStatus(USART3,USART_FLAG_TC))); //等待判断发送寄存器为空
    USART_SendData(USART3,*data++);
  }
  while(!(USART_GetFlagStatus(USART3,USART_FLAG_TC)));  //等待字符串最后一个数据发送完毕
  RE_DE=0;    //关闭发送,默认接收
}
/********************************************************************
函数功能:串口1发送一串字符数据
参数1:data:你要发送的数据
参数2:无
返回值:无
作者:xxx
********************************************************************/
void RS485_SendLenString(u8 *data,u16 len)
{
  while(len)   //循环发送指定长度的数据
  {
    //printf("%d\r\n",*data);
    RS485_sendbyte( *data++);
    len--;
  }
}
/*
typedef struct
{ 
  u8 head;  //帧头----0xfd
  u16 id;   //表示从机的编号
u8  cmd;  //控制字,比如0xff就是从机上传温度,光照、有毒其它等等数据  0:不采集数据,1:温度  2:光照 3:有毒气体 0xff:所有的
  u8 len;   //数据长度
  u8 buf[256];  //发送的数据
  u8 check;   //校验码
  u8 end;     //帧尾
}Slave;   //从机协议
*/
Slave s1={0xfd,1,0,{0},0,0xfe};   //定义结构体,并初始化
// 原来的
//接收中断+空闲中断
struct U3_DATA  rs485={0,0,0};  //定义结构体变量,同时初始化为0
//接收中断:每收到一个字符数据就会执行一次RS485_IRQHandler中断服务函数
//空闲中断:收完数据之后,串口产生空闲,会自动执行一次RS485_IRQHandler中断服务函数
void USART3_IRQHandler(void)
{
  if(USART_GetITStatus(USART3, USART_IT_RXNE)==SET)   //接收中断,存储数据
  {
    rs485.buf[rs485.len]=USART_ReceiveData(USART3);  //读数据并清除中断标志位
    rs485.len++;  //自增,为下个数据存储做准备 
  }else if(USART_GetITStatus(USART3, USART_IT_IDLE)==SET)  //空闲中断,接收数据结束
  {
    USART_ClearITPendingBit(USART3, USART_IT_IDLE);  //读SR
    USART_ReceiveData(USART3);   //读DR
    rs485.ok_flag=1;          //接收完成
    rs485.buf[rs485.len]='\0';  //添加结束符
  }
}
//发送数据包
void RS485_SendDatapackage(void)
{
  RS485_sendbyte(s1.head);  //发送帧头
  RS485_sendbyte(s1.id);    //从机ID
  RS485_sendbyte(s1.len);   //数据长度
  RS485_SendLenString(s1.buf,s1.len); //发送指定长度数据
  RS485_sendbyte(s1.check); //校验码
  RS485_sendbyte(s1.end);   //帧尾
}

从机的main.c

#include "stm32f10x.h"     
#include "delay.h"     
#include "beep.h" 
#include "led.h" 
#include "key.h" 
#include "usart1.h"
#include "cm3_bit.h"
#include "dht11.h"
#include "my_oled.h"
#include "image.h"
#include "w25q16.h"
#include "gz.h"
#include "adc1.h"
#include "dma1.h"
#include "string.h"
#include "rs485.h"
#include "tim4.h"
#include "pwm.h"
#define BEEP PCout(3)
int main(void)  //主函数  
{
  int i;
  char buf=0;
  float cpu_temp,mq135_val,mq2_val,gz_val;  //转换后的CPU温度
  u8 size_len=0;
  u16 cpu_val=0;  //得到CPU的ad值
  u16 dma_buf[4];   //保存DMA搬运数据
  u8 temp=0;
  u8 data_buf[4]={0};
  float wendu=0,shidu=0;
  u8 wsd_buf[32];
  NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);  //组2:2位抢占,2位响应
  systick_init(); //系统滴答延时
  BEEP_Init();    //beep初始化
  //LED_Init();   //LED灯
  //KEY_Init();   //普通按键
  USART1_Init(115200);  //串口1
  DHT11_Init();  //温湿度初始化
  DHT11_GetTempHumidity(data_buf);
  OLED_Init();   //OLED初始化
  W25Q16_Init();
//  EXTI_KEY_Init();  //外部中断-PA0  
//  for(i=0;i<15;i++) 
//  {
//    delay_ms(1000);
//    if(FONT_FLAG==1) break;
//  }
//  
//  if(FONT_FLAG==1 && i < 10)  //开机10S内按下按键,触发下载字库操作,执行下载功能
//  {
//    
//    W25Q16_Write_GB2312();    //下载字库
//  
//  }
  //CPU_MQ2_135_GZ_ADC1Init();   //PA2-MQ2:ADC1—_IN2     PA1-MQ135:ADC1—_IN1 
  //CPU_DMA1_ADC1Init(&cpu_val);   //初始化启动ADC1的MDA1传输
  DMA1_ADC1Init(dma_buf);   //初始化启动ADC1的MDA1传输
  //TIM4_Init(7200,10000);   //定时1S
  //LED1_PWM_Init(7200,100);   //100的意思是,计数100,就会和PWM的比较寄存器进行比较100次,
  RS485_Init(115200);
  while (1) 
  {   
    if(rs485.ok_flag==1)  //串口收到数据   
    {
      rs485.ok_flag=0;
      if(rs485.buf[1]==s1.id)  //说明主机呼叫对应的从机编号 
      {
        printf("从机%d收到主机的呼叫\r\n",s1.id);
        if(rs485.buf[0]==s1.head && rs485.buf[3]==s1.end)  //对比帧头帧尾
        {
          if(rs485.buf[2]==0)  //不采集数据
          {
          }else if(rs485.buf[2]==1)  //温湿度
          {
          }else if(rs485.buf[2]==2)  //光照
          {
          }else if(rs485.buf[2]==3) //有毒气体
          {
          }else if(rs485.buf[2]==0xff)  //温湿度+光照+有毒气体
          {
                        //采集温湿度   
            delay_ms(1500);
            temp=DHT11_GetTempHumidity(data_buf);
            if(temp==0)
            {
              //printf("采集校验ok\r\n");
              shidu=data_buf[0]+(data_buf[1]/100.0);          //56.000000
              wendu=data_buf[2]+(data_buf[3]/100.0);          //28.560000
              printf("从机%d:温度:%.2f  湿度:%.2f\r\n",s1.id,wendu,shidu);
            }
            shidu=data_buf[0]+(data_buf[1]/100.0);   //
            wendu=data_buf[2]+(data_buf[3]/100.0);   //31.00-------"31.00"
            //printf("温度:%.2f  湿度:%.2f\r\n",wendu,shidu);
            sprintf(wsd_buf,"温度:%.2f 湿度:%.2f",wendu,shidu);  //转换 wd_buf[]="温度:31.00";            
            OLED_Show_ChineseChar(0,0,wsd_buf);  //OLED显示转换数据
            //填充温湿度数据到数据包内
            s1.buf[0]=data_buf[0];
            s1.buf[1]=data_buf[1];
            s1.buf[2]=data_buf[2];
            s1.buf[3]=data_buf[3];
            //填充DMA搬运的cpu温度、mq135、mq2、光照
            s1.buf[4]=dma_buf[0]>>8;   //填充高位
            s1.buf[5]=dma_buf[0];
            s1.buf[6]=dma_buf[1]>>8;
            s1.buf[7]=dma_buf[1];
            s1.buf[8]=dma_buf[2]>>8;
            s1.buf[9]=dma_buf[2];
            s1.buf[10]=dma_buf[3]>>8;
            s1.buf[11]=dma_buf[3];
            s1.buf[12]='\0';
            s1.len=12;  //数据有效长度
            //校验码:数据的累积和,得出校验码
            s1.check=0;
            for(i=0;i<12;i++) 
            {           
              s1.check+=s1.buf[i];      
            }
            //printf("从机校验码:%d\r\n",s1.check);
            RS485_SendDatapackage();  //发送填充好的数据包
            printf("从机%d返回数据完成\r\n",s1.id);
          }
        }
      }
      rs485.ok_flag=0;
      rs485.len=0;
      memset(rs485.buf,0,256);  //清0
    }
/*    
    for(i=0;i<100;i++) 
    {
      delay_ms(10);
      TIM_SetCompare4(TIM3,i); //修改占空比,低电平占空比不断增加,越来越亮
    }
    for(i=100;i>=0;i--) 
    {
      delay_ms(10);
      TIM_SetCompare4(TIM3,i); //修改占空比,低电平占空比不断减少,越来越暗
    }
  */
    //作业1:完成PWM控制LED1
    //作业2:完成STM32F4的芯片PAck包安装,否则无法建立工程选择芯片
    //作业3: 下载STM32F4的新建工程文件,用于新建工程使用,然后寻找里面的文件建立F4的工程
/*    
    if(u1.ok_flag==1)  //串口收到数据   时:20
    {
      if(u1.buf[0] == *"时" && u1.buf[1] == *("时"+1))     //说明收到修改小时
      {
        size_len=u1.len-3;  //算出是一位数字还是2位数字
        if(size_len==1)  //说明为一位数字0-9
        {
          Hour=u1.buf[u1.len-1]-48;  //转换为整数        
        }else{  //两位数
          temp=(u1.buf[u1.len-2]-48)*10+(u1.buf[u1.len-1]-48) ;  //转换为整数,再合并  
          if(temp<24)Hour=temp;
        }
      }else if(u1.buf[0] == *"分" && u1.buf[1] == *("分"+1))  //说明收到修改分钟
      {
        size_len=u1.len-3;  //算出是一位数字还是2位数字
        if(size_len==1)  //说明为一位数字0-9
        {
          Min=u1.buf[u1.len-1]-48;  //转换为整数       
        }else{  //两位数
          temp=(u1.buf[u1.len-2]-48)*10+(u1.buf[u1.len-1]-48) ;  //转换为整数,再合并  
          if(temp<60)Min=temp;          
        }
      }else if(u1.buf[0] == *"秒" && u1.buf[1] == *("秒"+1))  //说明收到修改秒
      {
        size_len=u1.len-3;  //算出是一位数字还是2位数字     
        if(size_len==1)  //说明为一位数字0-9
        {
          Sec=u1.buf[u1.len-1]-48;  //转换为整数       
        }else{  //两位数
          temp=(u1.buf[u1.len-2]-48)*10+(u1.buf[u1.len-1]-48) ;  //转换为整数,再合并  
          if(temp<60)Sec=temp;            
        }
      }
      u1.ok_flag=0;
      u1.len=0;
      memset(u1.buf,0,256);  //清0
    }
*/
//    delay_ms(1000);
//    cpu_temp= ((1.43 - 3.3/4096*dma_buf[0])/0.0043)+25;
//    mq135_val= dma_buf[1]/4096.0*100;
//    mq2_val=  dma_buf[2]/4096.0*100;
//    gz_val=  100-(dma_buf[3]/4096.0*100);
//    
//    printf("CPU:%.2f ℃\r\n",cpu_temp);  //
//    printf("mq135:%.2f%%\r\n",mq135_val);  //
//    printf("mq2:%.2f%%\r\n",mq2_val);  //
//    printf("gz:%.2f%%\r\n",gz_val);  //
    //DMA作业:使用DMA完成多通道的数据搬运,DMA搬运CPU+光敏+烟雾+有毒气体数据
/*
    for(i=0;i<3;i++) adc1buf[i]=get_val(ADC1, i+1);
    adc1buf[0]=adc1buf[0]/4096*100;
    adc1buf[1]=adc1buf[1]/4096*100;
    adc1buf[2]=100-(adc1buf[2]/4096*100);
    printf("CPU:%.2f\r\n",get_cpu_val());
    printf("有毒气体:%.2f\r\n",adc1buf[0]);   //光照强度
    printf("烟雾甲烷:%.2f\r\n",adc1buf[1]);   //光照强度
    printf("光照:%.2f\r\n",adc1buf[2]);   //光照强度
*/    
    //作业1:完成光照强度的OLED 屏幕显示
    //作业2:完成烟雾+有毒气体的数据采集
    //要求:采用多通道采集
    //CPU温度采集----ADC1_IN16
/*    
    tem=DHT11_GetTempHumidity(data);
    if(tem==0)
    {
      //printf("采集校验ok\r\n");
      shidu=data[0]+(data[1]/100.0);   //
      wendu=data[2]+(data[3]/100.0);   //31.00-------"31.00"
      //printf("温度:%.2f  湿度:%.2f\r\n",wendu,shidu);
      sprintf(wd_buf,"温度:%.2f",wendu);  //转换 wd_buf[]="温度:31.00";
      sprintf(sd_buf,"湿度:%.2f",shidu);  //转换
      OLED_Show_ChineseChar(0,0,wd_buf);  //OLED显示转换数据
      OLED_Show_ChineseChar(0,2,sd_buf);          
    }
    delay_ms(1200);
*/  
/*    
    for(i=0;i<8;i++)  //连续显示图片,构成gif
    {
      OLED_ShowPicture(30,0,94,8,xj[i]);   //显示图片
      delay_ms(300);
    }
*/    
/*    
    OLED_ShowChinese(0,0,0,16);//中
    OLED_ShowChinese(18,0,1,16);//景
    OLED_ShowChinese(36,0,2,16);//园
    OLED_ShowChinese(54,0,3,16);//电
    OLED_ShowChinese(72,0,4,16);//子
    OLED_ShowChinese(90,0,5,16);//科
    OLED_ShowChinese(108,0,6,16);//技
    OLED_ShowString(8,16,"ZHONGJINGYUAN",16);
    OLED_ShowString(20,32,"2014/05/01",16);
    OLED_ShowString(0,48,"ASCII:",16);  
    OLED_ShowString(63,48,"CODE:",16);
    OLED_ShowChar(48,48,t,16);//显示ASCII字符    
    t++;
    if(t>'~')t=' ';
    OLED_ShowNum(103,48,t,3,16);
    OLED_Refresh();
    delay_ms(500);
    OLED_Clear();
    OLED_ShowChinese(0,0,0,16);  //16*16 中
    OLED_ShowChinese(16,0,0,24); //24*24 中
    OLED_ShowChinese(24,20,0,32);//32*32 中
    OLED_ShowChinese(64,0,0,64); //64*64 中
    OLED_Refresh();
    delay_ms(500);
    OLED_Clear();
    OLED_ShowString(0,0,"ABC",12);//6*12 “ABC”
    OLED_ShowString(0,12,"ABC",16);//8*16 “ABC”
    OLED_ShowString(0,28,"ABC",24);//12*24 “ABC”
    OLED_Refresh();
    delay_ms(500);
    OLED_ScrollDisplay(11,4);
*/
    //{
    //}
/*    
    GPIO_SetBits(GPIOC,GPIO_Pin_3);  //输出高电平--关LED
    delay_ms(500);
    GPIO_ResetBits(GPIOC,GPIO_Pin_3);
    delay_ms(500);
    if(u1.ok_flag==1)  //说明串口接收完数据
    {
      printf("%s",u1.buf);  //发送一串数据到电脑回显
      //清0
      u1.ok_flag=0;
      u1.len=0;
      memset(u1.buf,0,sizeof(u1.buf));  
    }   
*/  
/*    
//一直要做的事情   
    //  呼吸灯   
    for(i=0;i<1500;i++)  //渐亮
    {
      GPIO_ResetBits(GPIOC,GPIO_Pin_5);  //输出低电平
      GPIO_ResetBits(GPIOB,GPIO_Pin_1);  //输出低电平
      delay(i);
      GPIO_SetBits(GPIOC,GPIO_Pin_5);  //输出高电平--关LED
      GPIO_SetBits(GPIOB,GPIO_Pin_1);  //输出高电平--关LED
      delay(1500-i);    
    }
    for(i=0;i<1500;i++)  //渐暗
    {
      GPIO_ResetBits(GPIOC,GPIO_Pin_5);  //输出低电平
      GPIO_ResetBits(GPIOB,GPIO_Pin_1);  //输出低电平
      delay(1500-i);
      GPIO_SetBits(GPIOC,GPIO_Pin_5);  //输出高电平--关LED
      GPIO_SetBits(GPIOB,GPIO_Pin_1);  //输出高电平--关LED
      delay(i);   
    }
*/
  }
}

10、PWM

10.1 定义

脉冲宽度调制(PWM) 是一种数字信号,最常用于控制电路。该信号在预定义的时间和速度中设置为高(5v或3.3v)和低(0v)。通常,我们将PWM的高电平称为1,低电平为0

10.2 主要参数

10.2.1 PWM占空比

PWM信号保持高电平的时间百分比称为占空比。如果信号始终为高电平,则它处于100%占空比,如果它始终处于低电平,则占空比为0%。T1为占空比,T为一个PWM周期。

2345_image_file_copy_114.jpg

10.2.2 PWM的频率

频率决定PWM完成一个周期的速度。STM3编译器上面可以选择5MHZ,10MHZ,20MHZ和50MHZ。

10.3 PWM产生的方式

通过32控制板,有两个方式可以产PWM信号,一个是利用普通端口输出,另一个就是使用定时器的PWM端口或者服用IO口

10.3.1 普通IO口和PWM口方式介绍

  •   PWM端口

STM32 的定时器除了 TIM6 和 7。其他的定时器都可以用来产生 PWM 输出。其中高级定时器 TIM1 和 TIM8 可以同时产生多达 7 路的 PWM 输出。而通用定时器也能同时产生多达 4路的 PWM 输出,这样,STM32 最多可以同时产生 30 路 PWM 输出。

  •  普通IO口

一般能够输出PWM的端口都会在主要功能那一栏出现CHx的标志,而普通定时器没有出现这种标志。如图所示,上面的红框就是普通的定时器,不是专用的PWM端口。  

2345_image_file_copy_115.jpg

10.4 PWM程序

#include "pwm.h"                  // Device header
//led1 PF6--TIM10-CH1
void LED1_PWM_Init(u16 psc,u16 arr)  
{
  TIM_OCInitTypeDef tim10_pwm;   //比较输出 
  TIM_TimeBaseInitTypeDef tim10;  //基本定时
  GPIO_InitTypeDef led1;
  RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOF, ENABLE);  //使能GPIOC
  RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM10, ENABLE);
  GPIO_PinAFConfig(GPIOF,GPIO_PinSource6,GPIO_AF_TIM10); //映射复用为TIM10_CH1
  led1.GPIO_Mode=GPIO_Mode_AF;   //输出
  led1.GPIO_OType=GPIO_OType_PP;  //推挽
  led1.GPIO_Pin=GPIO_Pin_6; 
  led1.GPIO_PuPd=GPIO_PuPd_NOPULL;   //上拉
  led1.GPIO_Speed=GPIO_Speed_100MHz;
  GPIO_Init(GPIOF,&led1);
  tim10.TIM_ClockDivision=TIM_CKD_DIV1;       //72M/1=72主频
  tim10.TIM_CounterMode=TIM_CounterMode_Up; //向上计数
  tim10.TIM_Period=arr;         //重装载值
  tim10.TIM_Prescaler=psc-1;    //预分频
  TIM_TimeBaseInit(TIM10, &tim10);   //初始化定时器3的配置
  tim10_pwm.TIM_OCMode=TIM_OCMode_PWM1; 
  tim10_pwm.TIM_Pulse=0;   //比较寄存器的值,先写0,后面通过手动调用函数修改比较寄存器的值就是修改占空比
  tim10_pwm.TIM_OutputState=TIM_OutputState_Enable;   //使能输出控制
  tim10_pwm.TIM_OCPolarity=TIM_OCPolarity_Low;  //低电平有效
  TIM_OC1Init(TIM10, &tim10_pwm);  //初始化比较通道1
  TIM_OC1PreloadConfig(TIM10, TIM_OCPreload_Enable);  //使能重装载值比较寄存器
  TIM_Cmd(TIM10, ENABLE);  //定时器开始工作
}
//led2 PF9--TIM14-CH1
void LED2_PWM_Init(u16 psc,u16 arr)  
{
  TIM_OCInitTypeDef tim14_pwm;   //比较输出 
  TIM_TimeBaseInitTypeDef tim10;  //基本定时
  GPIO_InitTypeDef led1;
  RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOF, ENABLE);  //使能GPIOC
  RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM14, ENABLE);
  GPIO_PinAFConfig(GPIOF,GPIO_PinSource9,GPIO_AF_TIM14); //映射复用为TIM10_CH1
  led1.GPIO_Mode=GPIO_Mode_AF;   //输出
  led1.GPIO_OType=GPIO_OType_PP;  //推挽
  led1.GPIO_Pin=GPIO_Pin_9; 
  led1.GPIO_PuPd=GPIO_PuPd_NOPULL;   //上拉
  led1.GPIO_Speed=GPIO_Speed_100MHz;
  GPIO_Init(GPIOF,&led1);
  tim10.TIM_ClockDivision=TIM_CKD_DIV1;       //168M/1=168m主频
  tim10.TIM_CounterMode=TIM_CounterMode_Up; //向上计数
  tim10.TIM_Period=arr;         //重装载值
  tim10.TIM_Prescaler=psc-1;    //预分频
  TIM_TimeBaseInit(TIM14, &tim10);   //初始化定时器3的配置
  tim14_pwm.TIM_OCMode=TIM_OCMode_PWM1; 
  tim14_pwm.TIM_Pulse=0;   //比较寄存器的值,先写0,后面通过手动调用函数修改比较寄存器的值就是修改占空比
  tim14_pwm.TIM_OutputState=TIM_OutputState_Enable;   //使能输出控制
  tim14_pwm.TIM_OCPolarity=TIM_OCPolarity_Low;  //低电平有效
  TIM_OC1Init(TIM14, &tim14_pwm);  //初始化比较通道1
  TIM_OC1PreloadConfig(TIM14, TIM_OCPreload_Enable);  //使能重装载值比较寄存器
  TIM_Cmd(TIM14, ENABLE);  //定时器开始工作
}

11、W25Q16/64

11.1 简介

W25Q64是为系统提供一个最小空间、最少引脚,最低功耗的串行Flash存储器,25Q系列比普通的串行Flash存储器更灵活,性能更优越。

W25Q64支持双倍/四倍的SPI,可以储存包括声音、文本、图片和其他数据;芯片支持的工作电压 2.7V 到 3.6V,正常工作时电流小于5mA,掉电时低于1uA,所有芯片提供标准的封装。

W25Q64的内存空间结构:  一页256字节,4K(4096 字节)为一个扇区,16个扇区为1块,容量为8M字节,共有128个块,2048 个扇区。  

W25Q64每页大小由256字节组成,每页的256字节用一次页编程指令即可完成。

擦除指令分别支持: 16页(1个扇区)、128页、256页、全片擦除。

W25Q64支持标准串行外围接口(SPI),和高速的双倍/四倍输出,双倍/四倍用的引脚:串行时钟、片选端、串行数据 I/O0(DI)、I/O1(DO)、I/O2(WP)和 I/O3(HOLD)。

SPI 最高支持 80MHz,当用快读双倍/四倍指令时,相当于双倍输出时最高速率160MHz,四倍输出时最高速率 320MHz。这个传输速率比得上8位和16位的并行Flash存储器。

W25Q64支持 JEDEC 标准,具有唯一的 64 位识别序列号,方便区别芯片型号

11.1.1 w25q64芯片介绍

●SPI串行存储器系列    

-W25Q64:64M 位/8M 字节    

-W25Q16:16M 位/2M 字节    

-W25Q32:32M 位/4M 字节    

-每 256 字节可编程页

●灵活的4KB扇区结构    

-统一的扇区擦除(4K 字节)    

-块擦除(32K 和 64K 字节)

-一次编程 256 字节

-至少 100,000 写/擦除周期

-数据保存 20 年

●标准、双倍和四倍SPI

-标准 SPI:CLK、CS、DI、DO、WP、HOLD        

-双倍 SPI:CLK、CS、IO0、IO1、WP、HOLD        

-四倍 SPI:CLK、CS、IO0、IO1、IO2、IO3

●高级的安全特点

-软件和硬件写保护

-选择扇区和块保护

-一次性编程保护(1)

-每个设备具有唯一的64位ID(1)

●高性能串行Flash存储器    

-比普通串行Flash性能高6倍          

-80MHz时钟频率          

-双倍SPI相当于160MHz        

四倍SPI相当于320MHz        

-40MB/S连续传输数据    

-30MB/S随机存取(每32字节)    

-比得上16位并行存储器

●低功耗、宽温度范围

-单电源 2.7V-3.6V

-工作电流 4mA,掉电<1μA(典型值)

-40℃~+85℃工作

11.2 引脚

如下介绍的是W25Q64编著的SPI接口

2345_image_file_copy_116.jpg

2345_image_file_copy_117.jpg

11.2.1 SPI片选(/CS)引脚用于使能和禁止芯片操作

CS引脚是W25Q64的片选引脚,用于选中芯片;当CS为高电平时,芯片未被选择,串行数据输出(DO、IO0、IO1、IO2 和 IO3)引脚为高阻态。未被选择时,芯片处于待机状态下的低功耗,除非芯片内部在擦除、编程。当/CS 变成低电平,芯片功耗将增长到正常工作,能够从芯片读写数据。上电后, 在接收新的指令前,/CS 必须由高变为低电平。上电后,/CS 必须上升到 VCC,在/CS 接上拉电阻可以完成这个操作

11.2.2 串行数据输入、输出和 IOs(DI、DO 和 IO0、IO1、IO2、IO3)

W25Q64、W25Q16 和 W25Q32 支持标准 SPI、双倍 SPI 和四倍 SPI。

标准的 SPI 传输用单向的 DI(输入)引脚连续的写命令、地址或者数据在串行时钟(CLK)的上升沿时写入到芯片内。标准的SPI 用单向的 DO(输出)在 CLK 的下降沿从芯片内读出数据或状态。

11.2.3 写保护(/WP)

写保护引脚(/WP)用来保护状态寄存器。和状态寄存器的块保护位(SEC、TB、BP2、BP1 和BP0)和状态寄存器保护位(SRP)对存储器进行一部分或者全部的硬件保护。/WP 引脚低电平有效。当状态寄存器 2 的 QE 位被置位了,/WP 引脚(硬件写保护)的功能不可用。

11.2.4 串行时钟(CLK)

串行时钟输入引脚为串行输入和输出操作提供时序

11.3 W25Q64标准的SPI操作

W25Q64标准SPI总线接口包含四个信号: 串行时钟(CLK)、片选端(/CS)、串行数据输入(DI)和串行数据输出(DO)。

DI输入引脚在CLK的上升沿连续写命令、地址或数据到芯片内。

DO输出引脚在CLK的下降沿从芯片内读出数据或状态。

W25Q64分别支持SPI总线工作模式0和工作模式3。模式0和模式3的主要区别在于常态时的CLK信号不同;对于模式0来说,当SPI主机已准备好数据还没传输到串行Flash中时,CLK信号常态为低;

设备数据传输是从高位开始,数据传输的格式为8bit,数据采样从第二个时间边沿开始,空闲状态时,时钟线clk为高电平。

11.4 W25Q实例代码

#include "w25q64.h"                  // Device header
void W25Q64_SPI1_Init(void)
{
  GPIO_InitTypeDef  GPIO_InitStructure;
  SPI_InitTypeDef W25Q64_SPI1;
  RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB, ENABLE);  //使能GPIOB
  RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE);
  //PB3:SCK  PB4:MISO    PB5:MOSI
  GPIO_PinAFConfig(GPIOB, GPIO_PinSource3, GPIO_AF_SPI1); 
  GPIO_PinAFConfig(GPIOB, GPIO_PinSource4, GPIO_AF_SPI1);
  GPIO_PinAFConfig(GPIOB, GPIO_PinSource5, GPIO_AF_SPI1); 
  GPIO_InitStructure.GPIO_Mode=GPIO_Mode_AF;   //输出
  GPIO_InitStructure.GPIO_OType=GPIO_OType_PP;  //推挽
  GPIO_InitStructure.GPIO_Pin=GPIO_Pin_3|GPIO_Pin_4|GPIO_Pin_5; 
  GPIO_InitStructure.GPIO_PuPd=GPIO_PuPd_UP;   //上拉
  GPIO_InitStructure.GPIO_Speed=GPIO_Speed_100MHz;
  GPIO_Init(GPIOB,&GPIO_InitStructure);
  //PB14:CS
  GPIO_InitStructure.GPIO_Mode=GPIO_Mode_OUT;   //输出
  GPIO_InitStructure.GPIO_OType=GPIO_OType_PP;  //推挽
  GPIO_InitStructure.GPIO_Pin=GPIO_Pin_14; 
  GPIO_InitStructure.GPIO_PuPd=GPIO_PuPd_UP;   //上拉
  GPIO_InitStructure.GPIO_Speed=GPIO_Speed_100MHz;
  GPIO_Init(GPIOB,&GPIO_InitStructure); 
  GPIO_SetBits(GPIOB,GPIO_Pin_14);   //默认输出高电平 
  //初始化SPI1
  W25Q64_SPI1.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_8;  //8分频,168/8=21Mhz  
  W25Q64_SPI1.SPI_CPHA=SPI_CPHA_2Edge;  //第二个时钟沿采集数据
  W25Q64_SPI1.SPI_CPOL=SPI_CPOL_High;   //默认为高电平
  W25Q64_SPI1.SPI_CRCPolynomial= 7;   //CRC固定为7
  W25Q64_SPI1.SPI_DataSize=SPI_DataSize_8b;  //8位数据
  W25Q64_SPI1.SPI_Direction=SPI_Direction_2Lines_FullDuplex;  //全双工双向
  W25Q64_SPI1.SPI_FirstBit=SPI_FirstBit_MSB;  //高位先发
  W25Q64_SPI1.SPI_Mode=SPI_Mode_Master;     //主机模式
  W25Q64_SPI1.SPI_NSS=SPI_NSS_Soft;       //SPI_NSS_Soft,软件控制,GPIO控制片选拉高拉低
  SPI_Init( SPI1, &W25Q64_SPI1);
  SPI_Cmd(SPI1,ENABLE);  //使能SPI1开始工作
}
//发送一个字节数据
void SPI1_SendByte(char data)
{ 
  while(!(SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_TXE)));   //等待为空
  SPI_I2S_SendData(SPI1,data);  //发送数据
}
//接收一个字节数据
char SPI1_ReadByte(void)
{ 
  while(!(SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_RXNE)));  //判断非空
  SPI_I2S_ReceiveData(SPI1);  
}
/********************************************************************
函数功能:SPI1发送或接收1个字节的字符数据
参数1:data:你要发送的数据
返回值:接收到的数据
说明:你要发送一个字节数据出去的同时,也会接收得到一个无用的字节数据
      你要接收一个字节数据同时,也要发送一个无用的字节数据出去
********************************************************************/
char SPI1_SendReadByte(char data)
{ 
  while(!(SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_TXE)));   //等待为空
  SPI_I2S_SendData(SPI1,data);  //发送数据
  while(!(SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_RXNE)));  //判断非空
  SPI_I2S_ReceiveData(SPI1);  
}
/********************************************************************
函数功能:W25Q64写使能
参数1:无
参数2:无
返回值:无
作者:xxx
********************************************************************/
void W25Q64_WriteENABLE(void)
{ 
  CS=0;   //片选拉低
  SPI1_SendReadByte(0x06);//发送写使能指令 
  CS=1;//片选拉高
}
/********************************************************************
函数功能:读芯片ID
参数1:ID:存储数据的入口地址,第0字节为生产ID,第1字节为器件ID
参数2:无
返回值:无
W25Q16:0xef14
W25Q32:0xef15
W25Q64:0xef16
W25Q128:0xef17
********************************************************************/
void W25Q64_ReadID(u8 *ID)
{
  CS=0;   //片选拉低
  SPI1_SendReadByte(0x90);//发送读器件ID
  SPI1_SendReadByte(0x00);//发送24位地址
  SPI1_SendReadByte(0x00);//发送24位地址
  SPI1_SendReadByte(0x00);//发送24位地址
  ID[0]=SPI1_SendReadByte(0xff);//接收一个字节生产ID,的同时发送一个无用数据
  ID[1]=SPI1_SendReadByte(0xff);//接收一个字节器件ID,的同时发送一个无用数据
  CS=1;   //片选拉低
}
/********************************************************************
函数功能:读状态值
参数1:无
参数2:无
返回值:状态的值
作者:xxx
********************************************************************/
u8 W25Q64_ReadBUSY(void)
{
  u8 BUSY=0xff;
  CS=0;   //片选拉低
  SPI1_SendReadByte(0x05);//发送读状态指令0x05
  BUSY=SPI1_SendReadByte(0xff);//读取状态值
  CS=1;   //片选拉高
  return BUSY;
}
/********************************************************************
函数功能:擦除芯片
参数1:无
参数2:无
返回值:无
说明:擦除时间大概3s
********************************************************************/
void W25Q64_DeleteChip(void)
{
  W25Q64_WriteENABLE();  //写使能
  CS=0;   //片选拉低
  SPI1_SendReadByte(0xc7);//发送擦除芯片指令0xc7
  CS=1;   //片选拉高,结束
  while((W25Q64_ReadBUSY() & 1<<0));  //等待BUSY位为0,说明擦除完毕,并退出 
}
/********************************************************************
函数功能:块擦除
参数1:无
参数2:无
返回值:无
说明:擦除时间大概3s
********************************************************************/
void W25Q64_DeleteBlock(u8 Block)
{
  u32 addr=Block*65536;   //算出擦除块的起始地址
  W25Q64_WriteENABLE();  //写使能
  CS=0;   //片选拉低
  SPI1_SendReadByte(0xd8);//发送块擦除指令
  SPI1_SendReadByte((u8)(addr>>16));  //发送地址的高8位
  SPI1_SendReadByte((u8)(addr>>8));   //发送地址的中8位
  SPI1_SendReadByte((u8)addr);    //发送地址的低8位
  CS=1;   //片选拉高,结束
  while((W25Q64_ReadBUSY() & 1<<0));  //等待BUSY位为0,说明擦除完毕,并退出 
}
/********************************************************************
函数功能:擦除一个扇区,一共有32块*16扇区=512扇区
参数1:无
参数2:无
返回值:无
说明:擦除时间大概3s
********************************************************************/
void W25Q64_DeleteSector(u16 Sector)
{
  u32 addr=Sector*4096;   //算出擦除扇区起始地址
  W25Q64_WriteENABLE();  //写使能
  CS=0;   //片选拉低
  SPI1_SendReadByte(0x20);//发送扇区擦除指令
  SPI1_SendReadByte((u8)(addr>>16));  //发送地址的高8位
  SPI1_SendReadByte((u8)(addr>>8));   //发送地址的中8位
  SPI1_SendReadByte((u8)addr);    //发送地址的低8位
  CS=1;   //片选拉高,结束
  while((W25Q64_ReadBUSY() & 1<<0));  //等待BUSY位为0,说明擦除完毕,并退出 
}
/********************************************************************
函数功能:页编程写入数据,不能跨页编程,没有带擦除内存功能
参数1:data:写入数据的首地址
参数2:addr:数据写入的地址位置
参数3:len:数据写入的个数
返回值:无
说明:最多可以写入256个字节
********************************************************************/
void W25Q64_PageWriteNoTurn(u8 *data,u32 addr,u16 len)
{
  while((W25Q64_ReadBUSY() & 1<<0)); //判断芯片是否为忙
  W25Q64_WriteENABLE();  //写使能
  CS=0;   //片选拉低
  SPI1_SendReadByte(0x02);//发送页编程指令0x02
  SPI1_SendReadByte((u8)(addr>>16));  //发送地址的高8位
  SPI1_SendReadByte((u8)(addr>>8));   //发送地址的中8位
  SPI1_SendReadByte((u8)addr);    //发送地址的低8位
  while(len)  //循环写入
  {
    SPI1_SendReadByte(*data++);  //发送地址的高8位   
    len--;   //写入一个字节,数量减1
  }
  CS=1;   //片选拉高,结束
  while((W25Q64_ReadBUSY() & 1<<0));  //等待芯片为不忙,说明空闲了,写入数据完成
}
/********************************************************************
函数功能:页编程写入数据,可以跨页编程,没有带擦除内存功能
参数1:data:写入数据的首地址
参数2:addr:数据写入的地址位置
参数3:len:数据写入的个数
返回值:无
说明:最多可以写入256个字节
********************************************************************/
void W25Q64_PageWrite_Next_NoDelete(u8 *data,u32 addr,u32 len)
{
  u32 address_sum=addr;  //起始地址
  int i;
  u32 ago_num=256-(addr%256);  //算出前一页需要填充完的数量  
  u32 sum=len-ago_num;   //写入总数减去前面一页写的数量,剩下需要继续写的总数
  u32 zs_Page=sum/256;   //得到剩下总数的整倍页数
  u32 ys=sum%256;        //写完整页,剩下不满一页的余数
  //1、先写满前一页
  W25Q64_PageWriteNoTurn(data,addr,ago_num);  
  address_sum+=ago_num;  //地址增加
  data+=ago_num;      //写入的数据要随着地址一起增加
  //2、再写整倍页
  for(i=0;i<zs_Page;i++)
  {     
              //指针指向数据也要一起跨页
    W25Q64_PageWriteNoTurn(data,address_sum,256);    //写满页  
    data+=256;
    address_sum+=256;   //计算已写的地址数量   
  }
  //3、最后写完整页,剩下不满一页的余数
  W25Q64_PageWriteNoTurn(data,address_sum,ys); 
}
///********************************************************************
//函数功能:页编程写入数据,可以跨页编程,没有带擦除内存功能
//参数1:data:写入数据的首地址
//参数2:addr:数据写入的地址位置
//参数3:len:数据写入的个数
//返回值:无
//说明:最多可以写入256个字节
//********************************************************************/
//void W25Q64_PageWrite_Next_NoDelete(u8 *data,u32 addr,u32 len)
//{
//  u32 address_sum=addr;  //起始地址
//  int i;
//  
//  u32 ago_num=256-(addr%256);  //算出前一页需要填充完的数量  
//  u32 sum=len-ago_num;   //写入总数减去前面一页写的数量,剩下需要继续写的总数
//  u32 zs_Page=sum/256;   //得到剩下总数的整倍页数
//  u32 ys=sum%256;        //写完整页,剩下不满一页的余数
//  
//  //1、先写满前一页
//  W25Q64_PageWriteNoTurn(data,addr,ago_num);  
//  address_sum+=ago_num;  //地址增加
//  data+=ago_num;      //写入的数据要随着地址一起增加
//  
//  //2、再写整倍页
//  for(i=0;i<zs_Page;i++)
//  {     
//              //指针指向数据也要一起跨页
//    W25Q64_PageWriteNoTurn(data,address_sum,256);    //写满页  
//    data+=256;
//    address_sum+=256;   //计算已写的地址数量   
//  }
//  //3、最后写完整页,剩下不满一页的余数
//  W25Q64_PageWriteNoTurn(data,address_sum,ys); 
//  
//}
/********************************************************************
函数功能:读数据
参数1:data: 读到数据存放的首地址
参数2:addr:从W25Q64读数据的起始地址
参数3:len:读取数据的个数
返回值:无
作者:xxx
********************************************************************/
void W25Q64_ReadData(u8 *data,u32 addr,u16 len)
{
  W25Q64_WriteENABLE();  //写使能
  CS=0;   //片选拉低
  SPI1_SendReadByte(0x03);//发送读数据指令0x03
  SPI1_SendReadByte(addr>>16);  //发送地址的高8位
  SPI1_SendReadByte(addr>>8);   //发送地址的中8位
  SPI1_SendReadByte(addr);    //发送地址的低8位
  while(len)  //循环写入
  {
    *data++=SPI1_SendReadByte(0xff);  //发送地址的高8位  
    len--;   //读取的字节数量,每读一个,数量减1
  }
  *data='\0';   //添加数据结束符
  CS=1;   //片选拉高,结束
}

12.触摸屏

12.1实物图

2345_image_file_copy_118.jpg

12.2 触摸屏种类分类

  12.2.1 电阻式屏幕

特点:定位准确且可以实现单点触摸;电阻式屏幕是一种传感器,它将矩形区域的触摸点的物理位置转换为代表X/Y轴的电压,现在很多的LCD屏幕都是采用了电阻式的触摸屏,这种屏幕可以用5、6、7、8线来产生偏置电压,同时可以读回触摸点的电压。

电阻式触摸屏是一种传感器,基本上是薄膜加上玻璃的结构,薄膜和玻璃相邻的一面上均涂有ITO(纳米铟锡金属氧化物)涂层,ITO具有很好的导电性和透明性。当触摸操作时,薄膜下层的ITO会接触到玻璃上层的ITO,经由感应器传出相应的电信号,经过转换电路送到处理器,通过运算转化为屏幕上的X、Y值,而完成点选的动作,并呈现在屏幕上。

其优缺点也很明显,缺点是他只能支持单点触摸,不能实现多点触发触摸功能;但是由于这个单点触摸的特点,使得它触摸的精度很高。

image.jpeg

12.2.2 电感式触摸屏

支持多点触摸,价格偏贵。工业应用最广泛,我们现在市面上的手机都是感应式的触摸屏,它的优缺点是跟电阻式的触摸屏相反,优点是可以实现多点感应,而缺点就是精度没那么高。

2345_image_file_copy_120.jpg

12.2.3 红外线式

特点是:价格十分低廉,但其外框易碎,容易产生光干扰,曲面情况下失真,所以一般情况下我们不会选择这样的触摸屏。这个一般用在特殊的工艺。

12.2.4 表面声波式

特点是:可以解决各种缺点,但是屏幕表面如果有水滴和尘土会使触摸屏变的迟钝。

12.3 XPT2046采集触摸AD芯片

  12.3.1 触摸屏接口连接原理

2345_image_file_copy_121.jpg

12.3.2 XPT2046特性

工作电压范围为2.2V-5.25V,支持1.5V~5.25V的数字IO口,内建2.5V参考电压源;

电源电压测量(0V-6V)内建结温测量功能触摸压力测量;

采用SPI3线控制通信接口且有自动power-down功能;

封装:QPN-16.TSSOP-16和VFBGA-48与TSC2046、AK4182A完全兼容

12.3.3 XPT2046结构原理

2345_image_file_copy_122.jpg

12.3.4 XPT2046引脚

屏幕X/Y坐标的输入+/-端

2345_image_file_copy_123.jpg

2345_image_file_copy_124.jpg

2345_image_file_copy_125.jpg

应用引脚

2345_image_file_copy_126.jpg

12.3.5 使用到的引脚

看原理图,我们知道,有红框圈起来的引脚要用我们进行配置:

2345_image_file_copy_127.jpg

T_MISO  PB2 引脚

       XPT2046的串行数据输出端。STM32的数据输入端,数据在DCLK的下降沿                                   移出,当CS高电平时为高阻状态,配置为输入模式,空闲为高。

T_PEN   PB1引脚

      笔中断输出,配置为输入模式,空闲模式为高电平;

T_CS     PC13引脚

    片选信号。控制转换时序和使能串行输入输出寄存器,高电平时ADC掉电;

T_MOSI   PF11引脚

    XPT2046的串行数据输入端,STM32的输出端,当CS为低电平时,数据在DCLK上升沿锁存进来;

T_SCK   PB0引脚

     XPT2046外部时钟信号输入,STM32的时钟信号输出;

小结

 STM32发数据的时候,片选拉低,DCLK拉高,把数据锁存在T_MOSI引脚传输出去;当STM32读数据的时候,片选拉低,DCLK拉低,读取T_MISO电平状态。

12.4 XPT2046控制说明

12.4.1 单端还是差分模式

选择VBAT、 Temp和AUX时可以配置为单端模式,作为触摸屏应用时,可以配置为差分模式,这可有效消除由于驱动开关的寄生电阻及外部的干扰带来的测量误差,提高转换准确

度。

2345_image_file_copy_128.jpg

12.4.2 笔中断输出

有人按下屏幕时,触摸屏下拉到地,也就是按下为低电平

2345_image_file_copy_129.jpg

12.4.3 控制字节

用于选择采集的是X还是Y轴的AD值,作用:控制字节由 DIN 输入的控制字如表 3 所示,它用来启动转换,寻址,设置 ADC 分辨率,配置和对 XPT2046 进行掉电控

制。

2345_image_file_copy_130.jpg

12.5 代码实例

扫描触摸屏

touch:存放读取到的x/y坐标值,返 回 值 : 0:有按下;1:无按下

********************************************************************************/ 
unsigned char Xpt2046_ScanTouch(Touch_Typedef *touch)
{
  Touch_Typedef ad;
    unsigned char ucRetVaule;
  if(T_PEN == 0)
    {       //判断触摸屏有没有被按下
    if(T_PEN == 0)
        {
      Xpt2046_ReadXYAD(&ad);
      touch->xval = ad.xval * Kx + Bx;
      touch->yval = ad.yval * Ky + By;
      ucRetVaule = 0;
    }
  }
    else
    {
    touch->xval=0xffff;
        touch->yval=0xffff;
        ucRetVaule = 1;
  }
  return ucRetVaule;
}

读取X/Y轴AD值,并滤波处理

void Xpt2046_ReadXYAD(Touch_Typedef *touch)
{
  unsigned char i, j;
  unsigned short adx[5], ady[5];
  unsigned short temp;
  for(i=0; i<5; i++){   //先采集多次数据
    adx[i] = Xpt2046_ReadAD(0xd0);
    ady[i] = Xpt2046_ReadAD(0x90);
  }
  for(i=0; i<5; i++)
    {   //先排序,找出最大和最小的值
    for(j=0; j<5-i-1; j++)
        {
      if(adx[j] > adx[j+1])
            {
        temp = adx[j];
        adx[j] = adx[j+1];
        adx[j+1] = temp;
      }
      if(ady[j] > ady[j+1])
            {
        temp = ady[j];
        ady[j] = ady[j+1];
        ady[j+1] = temp;
      }
    }
  }
  touch->xval = adx[2];
  touch->yval = ady[2];
}

读取轴AD值

unsigned short Xpt2046_ReadAD(unsigned char cmd)
{
  unsigned char vh, vl;
  T_CS = 0;       //拉低片选,选中器件,开始通信
  Xpt2046_WriteByte(cmd); //发送测量命令
  Delay_Us(1);      //等到芯片忙结束
  vh = Xpt2046_ReadByte();//读取数据的高位
  vl = Xpt2046_ReadByte();//读取数据的低位
  T_CS = 1;       //拉低片选,取消选中,结束通信
  return ((vh<<8|vl)>>4); //返回结果,其中只有12位有效
}

触摸屏校准函数

这个可以通用,需要的直接复制就好了

void Xpt2046_TouchAdjust(void)
{
  unsigned short lcd_pos[4][2] = {20, 20, 300, 20, 20, 460, 300, 460, };
  Touch_Typedef touch_pos[4];       //用来存放x,y的4个AD值
  unsigned char i, j;
  double len1 = 0.00f, len2 = 0.00f;
  Lcd_ShowString(30, 130, "touch adjust start", RED, WHITE, 16);
  while(1){
        for(i=0; i<4; i++){         //读取4个对应的触摸屏坐标
      for(j=0; j<30; j++){      //画一个十字架
        Lcd_DrawPoint(lcd_pos[i][0]-15+j, lcd_pos[i][1], RED);
        Lcd_DrawPoint(lcd_pos[i][0], lcd_pos[i][1]-15+j, RED);
      }
      printf("等待校验\r\n");
            while(T_PEN == 1);        //等待按下触摸屏 
      Delay_Ms(50);         //延时50ms待数据稳定
      printf("按下触摸屏\r\n");
            Xpt2046_ReadXYAD(&touch_pos[i]);//获得触摸屏测量的x,y轴数值
            while(T_PEN == 0);        //等待松开手
      Delay_Ms(200);
      for(j=0; j<30; j++){      //清掉十字架图标
        Lcd_DrawPoint(lcd_pos[i][0]-15+j, lcd_pos[i][1], WHITE);
        Lcd_DrawPoint(lcd_pos[i][0], lcd_pos[i][1]-15+j, WHITE);
      }
        }
        //校验坐标-计算点击的触摸点是否正确  如果不正确重新校准
        //水平两个点之间的距离比较    
        len1 = (float)sqrt((touch_pos[1].xval-touch_pos[0].xval)*(touch_pos[1].xval-touch_pos[0].xval) \
                     + (touch_pos[1].yval-touch_pos[0].yval)*(touch_pos[1].yval-touch_pos[0].yval));
        len2 = (float) sqrt((touch_pos[3].xval-touch_pos[2].xval)*(touch_pos[3].xval-touch_pos[2].xval) \
                     + (touch_pos[3].yval-touch_pos[2].yval)*(touch_pos[3].yval-touch_pos[2].yval));    
        if(((len1/len2)<0.95) || ((len1/len2)>1.05)){
            continue;   
        }
        //垂直两个点之间的距离比较
        len1 = (float)sqrt((touch_pos[2].xval-touch_pos[0].xval)*(touch_pos[2].xval-touch_pos[0].xval) \
                     + (touch_pos[2].yval-touch_pos[0].yval)*(touch_pos[2].yval-touch_pos[0].yval));
        len2 = (float)sqrt((touch_pos[3].xval-touch_pos[1].xval)*(touch_pos[3].xval-touch_pos[1].xval) \
                     + (touch_pos[3].yval-touch_pos[1].yval)*(touch_pos[3].yval-touch_pos[1].yval));    
        if(((len1/len2)<0.95) || ((len1/len2)>1.05)){
            continue; //点击的点不符合要求
        }   
        //对角线两个点之间的距离比较
        len1 = (float)sqrt((touch_pos[3].xval-touch_pos[0].xval)*(touch_pos[3].xval-touch_pos[0].xval) \
                     + (touch_pos[3].yval-touch_pos[0].yval)*(touch_pos[3].yval-touch_pos[0].yval));
        len2 = (float)sqrt((touch_pos[2].xval-touch_pos[1].xval)*(touch_pos[2].xval-touch_pos[1].xval) \
                     + (touch_pos[2].yval-touch_pos[1].yval)*(touch_pos[2].yval-touch_pos[1].yval));    
        if(((len1/len2)<0.95) || ((len1/len2)>1.05)){
            continue;
        } 
        //计算校准参数   Kx (Ky)--斜率;Bx(By) --偏移量
        //计算x映射 Xlcd = Kx * touch_x + Bx
        Kx = (float)(lcd_pos[1][0]-lcd_pos[0][0]) / (touch_pos[1].xval-touch_pos[0].xval);
        Bx = lcd_pos[0][0] - Kx*touch_pos[0].xval;
        //计算y映射 Ylcd = Ky*touch_y + By
        Ky = (float)(lcd_pos[2][1]-lcd_pos[0][1]) / (touch_pos[2].yval-touch_pos[0].yval);
        By = lcd_pos[0][1] - Ky*touch_pos[0].yval;     
        Lcd_Fill(0, 0, 320, 480, WHITE);
    Lcd_ShowString(30, 130, "touch adjust OK", RED, WHITE, 16);
    printf("校准参数 Ky=%f;Kx=%f;By=%d;Bx=%d;\r\n",Ky, Kx, By, Bx);
        Delay_Ms(1000);
        Delay_Ms(1000);
        Lcd_Fill(0, 0, 320, 480, WHITE);
        break;        
  } 
}
相关文章
|
1月前
|
安全 Unix Linux
【ARM】在NUC977上搭建基于boa的嵌入式web服务器
【ARM】在NUC977上搭建基于boa的嵌入式web服务器
|
2月前
|
物联网 编译器 测试技术
【嵌入式 交叉编译器】如何在 ARM 架构下选择和使用高版本交叉编译器
【嵌入式 交叉编译器】如何在 ARM 架构下选择和使用高版本交叉编译器
216 7
|
2月前
|
存储 机器学习/深度学习 人工智能
嵌入式中一文搞懂ARM处理器架构
嵌入式中一文搞懂ARM处理器架构
39 1
|
9月前
|
编译器 C语言
ARM与C语言的混合编程【嵌入式系统】
ARM与C语言的混合编程【嵌入式系统】
83 0
|
9月前
|
存储 芯片
ARM简单程序设计【嵌入式系统】
ARM简单程序设计【嵌入式系统】
102 0
|
缓存 Linux 编译器
ARM嵌入式开发——基础概念
ARM嵌入式开发——基础概念
176 0
|
NoSQL Ubuntu Linux
arm嵌入式gdb移植和搭建远程gdb调试运行环境
arm嵌入式gdb移植和搭建远程gdb调试运行环境
504 0
arm嵌入式gdb移植和搭建远程gdb调试运行环境
|
1月前
|
数据处理 编译器 数据库
x64 和 arm64 处理器架构的区别
x64 和 arm64 处理器架构的区别
33 0
【各种问题处理】X86架构和ARM架构的区别
【1月更文挑战第13天】【各种问题处理】X86架构和ARM架构的区别
|
4月前
|
缓存 API Android开发
一起学点ARM的微架构二?
一起学点ARM的微架构二?
79 1