代码实现 —— 基于 STM32 的可见光通信系统课程设计

  
  
目前课设已完成,2m距离,传输10000个连续数字,每个数字两字节大小,即总共20000个字节160000bit,用时7s,大约2.3万bit/s,即22.4kB/s,误码率为0,视频演示链接

另外,自己写了一个基于QT的串口上位机,结合USMART,可以非常方便修改参数,所有源码在文后,一开始做的时候资料难求,网上都是要收费的,希望不要有人把我的资料拿去卖啦,分享分享,受益于互联网,回馈于互联网,如果你也有这个课设任务,希望对你有所帮助

  
在这里插入图片描述

在这里插入图片描述
  


  课程设计题目如下:

  关键字:STM32、可见光、距离1.5米、1万个连续数字、记录接收时间、通信速率、误码率。
  
  本文说明:只是通过C程序简单实现通信过程,不涉及高深的理论,所使用的模块也是廉价实惠的,争取以最低成本通过代码学习、理解并实现可见光通信即可。(可能废话有点多了,可以看到文末那边的框图,整体的思路写在后面,所有的工程文件在文末,对应着代码来看最好)

  

  
  先看硬件部分需要什么

  在STM32板子部分,因为接收与发送要尽量同时进行,因此需要使用两个开发板分别进行收发(使用RTOS系统的话没试过,感觉会影响通信),一开始以为需要定义一个1万的大数组,但是没必要,直接使用for循环进行赋值发送就好,因此可以使用最简单的stm32核心板就好(网购8元左右),再加usb下载线或者STLINK就可以使用了。

  在发射部分,可以使用二极管白光LED就好了,通过白光的亮-暗表示二进制的0-1,适合在比较近距离的时候,再加一个灯罩效果会更佳,STM32普通的GPIO能够输出的电平最高只有3.3V,使用一个3W的白光LED也可以控制,不过最好是将LED的阳极接在单片机板载输出的5V引脚上,阴极则接到一个普通的GPIO上即可,该GPIO输出低就能让LED获得更大的发射功率,在远距离的情况下可以使用二极管激光LED,用法和白光LED的一样。
  
  在接收部分,使用一块钱左右的光敏电阻模块,该模块自带了信号放大电路和模数转换功能,通过感应周围光线的亮暗就能够将0、1电平通过引脚输出,因此只要将光敏模块的数字输出引脚接至STM32的一个普通GPIO上,STM32就能够通过读取该GPIO获知当前环境的光线是亮还是暗了(不要担心环境光线影响大,该模块自带一个滑动电阻可以调节灵敏度,只要设置好在当前环境下该模块输出为暗即可)。
  
  
  了解一些概念

  可见光,就直白的理解就是人眼能够看见的光都可以称之为可见光嘛,不一定非得是白光,红光、绿光、蓝光或者光纤都可以称为可见光,注意红外线紫外线是不可见光。

  信号调制和解调,也可以先简单地理解成以什么样的方式表示一个信号,不同方式可以代表不同的信号,而解调就是根据识别到的方式判断其为何种信号的过程,这更像是两者之间的一种协议。比如二进制0信号,使用1.6ms的高电平和0.7ms的低电平表示,二进制信号1可以使用1.6ms的高电平和1.5ms的低电平表示,相对于发送端来说,这些高低电平就体现在LED的暗-亮上,通过暗亮持续的时间不同代表不同的信号;而对于接收端来说,则需要能读取出与光敏模块连接的GPIO的电平持续时间,进而就可以判断出是什么信号。当然,还可以使用pwm不同占空比来代表不同信号,或者其他的论文里提到的方式。

  对于上面提到的读取引脚持续的电平时间,或者是记录接收用时,需要用到定时器,STM32有好多并且功能强大的定时器,相信只要学过stm32的朋友都有了解,关于定时器的理解,我通常会和别人解释为它就是一种计数器,你可以设置它的频率来控制它计数一次的时间,最终得到的计时时间实则为计数的次数乘以该时间,你还可以设置它计数到多少数值的时候让它引发中断,当然也可以在计数溢出的时候发生中断,溢出是因为stm32f1的定时器是16位的寄存器,最多只能计数2^16即65536次,溢出了它还能自动重装载,循环计数,最要命还有一点,你可以把它看成是与CPU并行执行、两者独立的,比如你开启定时器计数后,不会影响到CPU执行其他的代码,两者各司其职,是相互独立的,不同于延迟函数,在延迟期间,CPU不能去做其他事。

  对于串口通信就没什么好说的啦,就是开启了串口中断,如果需要写数据显示到电脑时,会把数据写入DR寄存器,此时就会引发串口中断,把数据传送出去,而电脑传数据回单片机时也会引发中断,只要与上位机定好协议就能够解析出接收到的数据是什么,这里可以直接使用正点原子的工程模板,自带了串口数据处理的功能,非常方便。另外,还可以使用正点原子自己写的一个串口调试组件USMART,有什么作用呢?比如在代码烧录运行后,你想要修改某个函数的传入参数时,就可以通过USMART来实现,免去了重复修改代码、编译、烧录的过程,其原理也是基于串口通信,将串口接收到的字符串解析出来,然后调用该函数并赋予函数入口参数新的数值。链接 -》第40讲 USMART调试组件实验-M3h

  
  
  进入正题,发射端的数据到底应该怎么发送?从上面已知,白光只有两种状态可以被识别,就是亮或者暗,因此传输所有的数据,都需要将该十进制数据转换为2进制,将二进制数据发送出去,接收端只要读取到该二进制数据,就意味着最简单的可见光通信完成了。所以有两件事需要做,一是将十进制转二进制,二是规定好以什么样的方式表示不同的信号,哦还有一件,就是一个数据需都需要什么信号呢?

  我的做法很简单,对于十进制转二进制,可以通过位操作的方式判断该数据的每一个位是什么就好了,因为需要传输1万个数字,所以需要定义 uint16_t 的数据类型,也就是 unsigned short int 数据类型,使用16位的二进制表示一个数据,可表示的范围是0到65535,对某个数据的位操作如下:

//转化一个数据(2字节)
void transData(uint16_t data)
{
	uint8_t j;
	uint16_t tmp;
	printf("%d  ",data);
	for(j=16;j>0;j--) 
	{
		tmp = (data)&(1<<(j-1)); //从第16位由高往低位发送
		if(tmp) printf("1");
		else printf("0");
	}
}
//比如提取data的第16位时,让1左移15位,则1就在第16位了,其后都是0
//再与data相与,那么只有data的第16为的数据可以被保留,其他的全都为0
//赋值给tmp,tmp的状态就是data的第16位状态了

  对于以什么样的方式表示不同信号,可以采取不同高电平持续时间的不同来区分不同的信号,比如我高电平持续t1时间表示0,高电平持续t2时间表示1,这样就能较快速并且方便地被接收端识别;与此同时,为了区分数据或者说避免外界信号的干扰,可以在每个数据前都再加上一个起始信号,比如可以用连续两次的高电平均持续t3时间来表示起始信号,即表示一个数据的开始。总共需要三种信号来表示一个数据。

  
  发送端的基本思路已解决,再看看接收端,其实已知多长高电平对应的是什么信号了,接收端需要做的,就是读取出该高电平时间,然后判断是什么信号就好。关键点在于,如何读取引脚的高电平时间。

  一开始的思路很狭隘,就是开启该引脚的外部中断,遇到上升沿就触发中断,然后接收端就进入一个延迟函数等待,等待若干时间后再读取引脚电平来区分信号的不同。其实可以使用定时器来完成这部分计时,好处就在于和上文提到的一样,定时器计时期间不会影响CPU的工作,因此可以使用输入捕获,其本质也是使用定时器计时的。比如可以这样配置定时器,先开启上升沿中断和更新中断,也就是当遇到一个高电平时会触发该中断,接着在该中断里开启定时器计数,并且同时将定时器设为下降沿中断,因此在本次高电平结束的时候,又一次触发该中断,然后再在该中断里停止定时器计数并读出定时器的计数数值,即可得到一次高电平捕获的时间,同时还要将定时器设为上升沿中断以备下次的捕获。总结来说,每一次获取一个高电平时,只需要在一个while循环里一直等待,直到捕获完一次高电平信号,就可以开始判断该信号是什么类型的信号了。

  
  

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  
对于以上框图的解释如下,从发射端到接收端:

  USMART是一个开源的串口调试组件,它可以通过串口助手调用程序里面的任何函数并执行,可以随意更改函数的输入参数(支持数字(10/16进制)、字符串、函数入口地址等作为参数)。当串口接收到数据时,USMART会将接收到的字符串解析,通过对应的函数指针即可访问到程序里的任意函数。因此加入USMART,可实现在芯片已经烧录好的情况下,在线修改发射相关的参数,非常方便。

  在发射端如果收到了发射连续数据的指令,则程序先将起始数据、结束数据和长度发射给接收端用于验证,随后再在for循环里将数据循环发射出去。对于发射一个数据时,则是通过循环移位逐个将二进制位提取出来并判断。
在接收端初始化了定时器3和定时器5,分别用来计算接收总用时和捕获光敏电阻引脚高电平持续时间的;初始化FSMC则是为了使用外部SRAM和LCD,两者均由FSMC来配置和管理。

  刚初始化完成,会先对缓冲数组清零,在后面接收数据期间,如果数据有错误,错误的数据不会被保存到数组里,那么该位则仍为0,以便在数据统计误码时,可以直接通过数组里的数值是否为0判断该接收数据位是否正确。
在每一次接收数据时,会先接收3个信息头,分别是数据块的起始数据、结束数据和数据长度,只有当这3个均接收正确并验证成功(比如结束数据+1-起始数据=长度),才会往下循环接收真正的数据。

  在循环接收数据期间,先给i赋值为起始位,如果数据没有出错,则i和所接收到的数值是一样的(每次循环步进相等),因此可以通过检验i和所接收的数据是否相等来确定该数据有没有出错。由于当接收端接收错误,比如光线被挡住了,但这并不影响发射端以固定的步进发射数据,而接收端则会因为读取不到数据,步进停止,因此在每次出现错误数据后,都需要下一次的正确数据来矫正接收端里的循环步进值。

  另外,在统计误码之后,需要对缓冲数组清零,否则在进行下一次数据接收与判断时会出错。

  
  
全部工程下载链接:
链接:https://pan.baidu.com/s/1r5pLaeCu5mieWKNu90GWDA
提取码:feng
跳转:可见光通信工程下载链接【提取码:feng】

  
【发射端main.c】

#include "stm32f10x.h"
#include "delay.h"
#include "sensor.h"
#include "usart.h"
#include "usmart.h"

/*
   程序整体思路:
	   ① 把每个要发射的数据转成二进制,再将二进制的每一位逐1发射出去;
		 ② 需要3个信号表示一个数据,分别用高电平持续的不同时间表示不同
		   信号:起始信号、0信号、1信号,分别对应p1,p2,p3;
		 ③ 默认状态LED是亮的,起始信号发射的时候,将LED引脚置高,并延迟
			 p1的时间,再进行翻转,两次这样的p1高电平时间即代表起始信号;
		 ④ 数据信号需要紧跟着起始信号,一次起始信号之后就是16位的0、1数
		   据信号,对于0信号,则是高电平持续p2时间,再翻转,对于1信号,
			 则是高电平持续p3时间;
		 ⑤ 因此只需要设置好每个信号之间的时间参数,使用滴答定时器计数的
		   延迟函数达到持续的效果,然后只要判断即将发射的数据的各个位,
			 即可将一个数据发射出去。
			 
 *	2020.8.30 —— by afeng
    1、优化程序,优化部分变量,让程序运行的更快,尽量与接收端同步。

 *	2020.8.25 —— by afeng
    1、优化发射端串口助手,使其能够实时自动刷新电脑可用的串口,当该串口掉线,则会自动关闭连接。
		2、优化串口助手的显示区域部分,通过设置光标的位置使得文字可以从上至下显示。

 *	2020.8.23 —— by afeng
    1、加入连续发射多个数据的函数,并支持不同起始数据和结束数据,然后
		   注册到USMART调试组件,以达到从串口助手来控制发射的数据。
		2、结合了自己的基于QT的串口助手,把本工程的USMART组件与该调试助手
		   结合起来使用,更方便调参。

 *	2020.8.20 —— by afeng
    1、开始使用USMART调试组件,可以在串口助手通过函数带参数的方式在线修改
		   各个参数,在后面不同距离下改变参数有着极大的便利性,免去了重复修改
			 、编译和烧录代码的繁琐过程。
	  2、解决了二进制数据位错误的BUG,是由于自己不够熟练位操作引起的,最后
		   决定使用数字1的移位操作来提取出每个数据的各个二进制位。
		3、实现了基于滴答定时器的长时间延迟,由于该定时器只有16位,基于ST库写
		   的延迟函数一次最大不能超过2s,经过额外编程实现能够一次延迟10s以上。
		4、剔除了每次数据都需要结束信号的方案,使得发射时间进一步提升,其实每
		   个数据的起始信号也可以作为每个数据的区分了。
			 
 *	2020.8.19 —— by afeng
    1、使用函数将十进制转二进制,并能够将每一个位通过亮-暗表示0-1将二进制
		   各个位成功发射出去,测得最小时间参数可以达到300us左右,但是由于接收
			 端的限制以及距离的影响,这个参数需要每次调整。
*/


//连续发射数据
void emitSeriesData(uint16_t start,uint16_t end)
{
	uint16_t i,len=end+1;
	if((start>0) && (end>start))
	{
		printf("发送从 %d 到 %d ,共 %d 个数据。\r\n",start,end,(end+1-start));
		printf("正在发送……\r\n");
		emitData(start); emitData(end); emitData((end+1-start)); //先发送本次数据块的信息
		delay_ms(10);
		for(i=start;i<len;i++) 
		{
			emitData(i); //开始循环发送,从开始数据位开始发送
			if(i%100 == 0) printf("%d ",i);
		}
		LED = 0;
		printf("\r\n本次已发送完成。\r\n");
	}
}

int main(void)
{	
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); 	 //设置NVIC中断分组2:2位抢占优先级,2位响应优先级
	uart_init(115200);	 //串口初始化为115200
	delay_init();	    	 //延时函数初始化	  
	LedConfigInit();  //初始化光源引脚及发射相关参数
	usmart_dev.init(SystemCoreClock/1000000); //初始化 USMART,实现在串口修改发射相关参数
	LED = 0;  //发射前默认光源开启
	printf("当前参数:\r\n"); lookUpCurrentParameter();
	printf("\r\n等待发射数据……\r\n");
	emitSeriesData(1,5);
	while(1)
	{
		 //直到等到发射命令从串口进入,才进行发射
		
	}
}

  
【发射端sensor.c】

#include "sensor.h"
#include "delay.h"
#include "usart.h"

#define DATABIT 14

uint32_t startSignalHold_time=600; //起始信号持续高电平
uint32_t start_Wait_time=800;   //发送二进制0时 高电平 持续的时间
uint32_t data_0_Hold_time=1000;  //发送二进制1时 高电平 持续的时间
//以下是低电平等待时间
uint32_t data_0_Wait_time=300;
uint32_t data_1_Hold_time=300;
uint32_t data_1_Wait_time=300;

timeParameter T = {0,0,0,0,0,0}; //用于延迟部分

//初始化光源引脚及发送参数
void LedConfigInit()
{
	GPIO_InitTypeDef  GPIO_InitStructure;
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
	
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; 		 //推挽输出
  GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;		 //IO口速度为50MHz
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4;	    		 
	GPIO_Init(GPIOA, &GPIO_InitStructure);	  				 
	GPIO_ResetBits(GPIOA,GPIO_Pin_4); //LED光源

}

//发送一个数据(2字节)
void emitData(uint16_t data)
{
	u8 j;
	u16 tmp;
	emitStartSignal();
	for(j=DATABIT;j>0;j--) //最大可表示到65535的数据
	{
		tmp = (data)&(1<<(j-1)); //由高位往低位发送
		if(tmp) {emitData_1();}
		else {emitData_0();}
	}
}

//配置发送参数,这样定义是为了方便调试,使用usmart
void configEmitParameter(u32 p1,u32 p2,u32 p3,u32 p4,u32 p5,u32 p6)
{
	startSignalHold_time = p1;
	data_0_Hold_time = p2;
	data_1_Hold_time = p3;
	start_Wait_time = p4;
	data_0_Wait_time = p5; 
	data_1_Wait_time = p6; 
}

//发射一个数据前,先发射一个起始信号
void emitStartSignal()
{
	LED = 1; emitProcessDelay_us(startSignalHold_time);
	LED = 0; emitProcessDelay_us(start_Wait_time);
	//LED = 1; emitProcessDelay_us(startSignalHold_time);
	//LED = 0; emitProcessDelay_us(start_Wait_time);
}

//发射1信号
void emitData_1()
{
	LED = 1; emitProcessDelay_us(data_1_Hold_time);
	LED = 0; emitProcessDelay_us(data_1_Wait_time);
}

//发射0信号
void emitData_0()
{
	LED = 1; emitProcessDelay_us(data_0_Hold_time);
	LED = 0; emitProcessDelay_us(data_0_Wait_time);
}

//发射结束信号
void emitEndSignal()
{
	LED = 0; emitProcessDelay_us(data_0_Hold_time);
	emitProcessDelay_us(data_0_Hold_time);
}

void lookUpCurrentParameter() //查看当前发射参数
{
	printf("HoldTime:     start: %d  0信号: %d  1信号: %d\r\nWaitTime:    start: %d  0信号: %d  1信号: %d\r\n",\
				startSignalHold_time,data_0_Hold_time,data_1_Hold_time,start_Wait_time,data_0_Wait_time,data_1_Wait_time);
}

//传入us的个数,支持到每次最大10s的延迟,即10000000us
void emitProcessDelay_us(uint32_t time) 
{
	uint8_t i;
	if(time<=23000) delay_us(time); //直接us延迟
	else if(time>23000 && time<1000000) //毫秒转化
	{
		T._ms = time/1000.0;
		T._1ms = (int)T._ms; //1ms的个数
		T._us = (T._ms-T._1ms)*1000; //us的数值
		delay_ms(T._1ms);
		if(T._us) delay_us(T._us);
	}
	else if(time >= 1000000) //大于1s的时候
	{
		T._s = time/1000000.0; //求出有多少个1000ms(即1s),因为delay_ms最大支持到1800ms
		T._1s = (int)T._s; //1s的个数
		T._ms = (T._s-T._1s)*1000; //ms的数值,肯定小于1000
		for(i=0;i<T._1s;i++) delay_ms(1000);
		if(T._ms) delay_ms(T._ms);
	}
}

  
【接收端:main.c】

#include "stm32f10x.h"
#include "usart.h"
#include "delay.h"
#include "sensor.h"
#include "usmart.h"
#include "timer.h"
#include "sram.h"	
#include <string.h>
#include "lcd.h"

/*
    程序整体思路:
		  ① 初始化光敏传感器接收引脚,将该引脚接至定时器5通道1即
	      PA0引脚,开启定时器5通道1的输入捕获功能,引脚初始为低电平;
			② 发射端发射的起始信号(光照由亮边暗,引脚由低变高)触发捕获
			  中断,定时器5开始计数,在中断里再将中断的上升沿触发改为下降沿
				触发,直到光照由暗变亮时(引脚由高变低)再一次触发中断,此次
				进入中断将CNT的值读取出来并计算,即接收端读取到的高电平时间;
			③ 接收数据前,程序在while(1)循环里等待捕获一次信号结束的标志位,
			  直到该标志位为真,则直接读取出时间并判断是否与发射端的该段高
				电平时间相近(在20ms以上基本一致,20ms以下有些偏差,因此设了
				向上向下的容许误差),每一次捕获到一个信号就读取时间判断该信号;
			④ 协议约定,两次高电平时间p1表示一个数据的起始信号,往后紧接的一次
			  高电平时间p2表示是0信号,再往后紧接的一次高电平时间p3表示1信号。


 *	2020.9.2 —— by afeng
	  1、拓展使用外部SRAM,先定义好一个大数组作为缓冲,将所有接收到的数据存放起来,
		   再进行误码验证,或者其他操作,减少了数据处理时间,让CPU能够更加专注去接收
			 新的数据。
		2、使用TFTLCD用于显示状态信息及接收结果。

 *	2020.8.30 —— by afeng
    1、优化程序,优化部分变量,让程序运行的更快,尽量与发射端同步。

 *	2020.8.25 —— by afeng
	  1、增加计时接收数据总用时的功能,使用定时器3,周期为10KHz,
		   最长可计时超过 1 小时。
		2、增加错误数据统计功能,其思路为:接收一次数据前,先得到发送端
			 即将发射数据块的起始数据、结束数据和总的长度,先接收三者并验证
			 计算,如果正确则进入for循环接收,在for循环里的条件和接收端的
			 一一致,因此在每次出现错误数据时,可以先记下错误的数据,直到
			 下一次正确数据出现,再拿前后两个相减以调整循环的当前值并统计
			 错误个数,达到与发射端同步的效果。即边接收边验证数据的正确。
		3、解决了当出现多次数据错误时统计错误个数的BUG,需要一个临时变量
		   用来记录错误区间以调整当前循环值。
			 
 *	2020.8.23 —— by afeng
	  1、优化接收数据的判断方法,以前是当检测到引脚出现电平变化时,开始
		   与接收端同样的延迟,延迟结束后若引脚电平出现如期的翻转,则认为
			 该信号有效;现改为使用输入捕获的方式进行检验,当引脚电平变化,
			 则会引起中断并开启定时器记录该引脚的高电平时间,直到引脚电平再
			 次翻转,则可以读取出定时器的时间以判断信号,分别有起始信号、0 
			 电平信号、1电平信号,分别采用不同高电平持续时间来判定。
		2、自己动手撸了一个基于QT的串口助手,结合本工程下的USMART调试组件
		   可以达到通过串口助手很方便地在线调整发射与接受端的各个参数,分
			 别有,发射端:起始信号、0信号、1信号的高电平时间,及其间隔低电
			 平时间,接收端:起始信号、0信号、1信号的高电平判断时间,及其判
			 断的上下限容许误差。此外,还可以指定发送数据的起始数据和结束数据。
		3、取消使用LCD显示接收的方案,因为显示一个数据太耗时间了。
		
 *	2020.8.20 —— by afeng
    1、开始使用USMART调试组件,可以在串口助手通过函数带参数的方式在线修改
		   各个参数,在后面不同距离下改变参数有着极大的便利性,免去了重复修改
			 、编译和烧录代码的繁琐过程。
		2、解决了偶尔出现数据连续接收错误的BUG,是因为在每次数据接收期间使用
		   printf函数打印太多数据,导致接收端与发射端的时间失调不同步,即发射
			 端发射起始信号时接收端还在处理printf函数,等待发射端发射数据时,接
			 收端又将数据信号当作起始信号来判断,进而导致错误。
		
 *	2020.8.19 —— by afeng
    1、能够接收16位的二进制信号并转为其对应的十进制数据。
		
*/


#define  dataLen 12000
uint16_t dataBuf[dataLen] __attribute__((at(Bank1_SRAM3_ADDR)));
uint16_t dataCurrentPos=0;

int main(void)
{	
	uint16_t recvWorkCounts=0;
	uint8_t j=0,startRecvFlag=0;
	uint16_t i=0,recvData,dataInfo[3]; //dataInfo用来检验一次数据接收的起始数据和结尾数据
	uint16_t errorCounts=0; //用来记录接收错误的数据个数
	uint32_t recvTime_us;  //用来记录接收用时
	
    delay_init();	    	 //延时函数初始化
	uart_init(115200);	 //串口初始化为115200
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); 	 //设置NVIC中断分组2:2位抢占优先级,2位响应优先级
	TIM3_CountTime_Init(); //用于计时接收数据的时间
    LdrConfigInit();  //初始化光敏传感器的引脚和输入捕获功能
	usmart_dev.init(SystemCoreClock/1000000); //初始化 USMART,实现在串口修改接收相关参数
	FSMC_SRAM_Init();			//初始化外部SRAM
	LCD_Init();

	memset(dataBuf,0,sizeof(dataBuf)); //全都初始化为0
	
	POINT_COLOR=RED;//设置字体为红色 
	LCD_ShowString(60,50,200,16,16,"My Class Design");	
	LCD_ShowString(60,70,200,16,16,"2020-9-4 @By afeng");	
	LCD_ShowString(60,90,230,16,16,"Visible Light Communication.");
	POINT_COLOR=BLUE;//设置字体为蓝色
	LCD_ShowString(60,160,200,16,24,"WorkCounts  :");
	LCD_ShowString(60,200,200,16,24,"errorCounts :"); //LCD_ShowNum(230,200,10000,5,24)
	LCD_ShowString(60,240,200,16,24,"usingTime/us:"); //LCD_ShowNum(230,240,200000000,9,24)
	POINT_COLOR=BLACK;
	LCD_ShowNum(230,160,0,9,24);
	LCD_ShowNum(230,200,0,9,24);
	LCD_ShowNum(230,240,0,9,24);
	
	printf("等待接收数据……\r\n");
	while(1)
	{
		recvData = receiveData();  //阻塞等待接收一个数据
		
		if((!startRecvFlag) && recvData)  //先接收起始数据和结束数据、及总的长度
		{
			dataInfo[j] = recvData;
			j++;
			if(j==3)
			{
				j = 0; //若结束+1-起始 = 长度,则准备进入接收数据
				if((dataInfo[1]+1-dataInfo[0]) == dataInfo[2]) startRecvFlag = 1; 
				else printf("此次接收失败!请重新发送数据!\r\n");
			}
		}
		
		if(startRecvFlag) //开始接收数据
		{
			printf("receiving...\r\n");
			startRecvFlag = 0;
			startCountRunningTime(); //开始计时
			for(i=dataInfo[0];i<(dataInfo[1]+1);i++) //循环接收数据
			{
				recvData = receiveData();  //阻塞接收
				if(recvData==i) dataBuf[dataCurrentPos++] = recvData;
				else 
				{
					if(recvData!=0) 
					{
						dataCurrentPos = recvData-i;
						dataBuf[dataCurrentPos++] = recvData;
						i = recvData;
					}
				}					
			}
			recvTime_us = getRunningTime(); //结束接收计时并得到时间
			
			printf("接收完成!正在检验数据……\r\n");  //检验数据
			dataCurrentPos=0;
			for(i=dataInfo[0];i<(dataInfo[1]+1);i++)
			{
				if(dataBuf[dataCurrentPos]!=i) 
				{
					errorCounts++;
					printf("【err】 ");
				}
				else printf("%d ",dataBuf[dataCurrentPos]);
				dataCurrentPos++;
			}
			recvWorkCounts++;
			printf("\r\n");
			printf("-----------------------------------------------------\r\n");
			printf("处理完成!  错误数据: %d 个  总接收用时: %d us (%.3f ms  -->  %.2f s)\r\n", \
			errorCounts,recvTime_us,(recvTime_us/1000.0),(recvTime_us/1000000.0));
			printf("-----------------------------------------------------\r\n\r\n");
			LCD_ShowNum(230,200,recvWorkCounts,9,24);
			LCD_ShowNum(230,200,errorCounts,9,24);
			LCD_ShowNum(230,240,recvTime_us,9,24);
			errorCounts = 0;
			recvData = 0;
			dataCurrentPos=0;
			memset(dataBuf,0,sizeof(dataBuf));
			printf("等待接收数据……\r\n");
		}
	}
}

  
【接收端 sensor.c】

#include "sensor.h"
#include "usart.h"
#include "timer.h"

#define DATABIT 14

extern u8  TIM5CH1_CAPTURE_STA;

uint32_t startSignal_time;
uint32_t data_0_time;
uint32_t data_1_time;
uint32_t upperAddValue;
uint32_t lowerDecValue;
//上下限时间,先定义变量再赋值,为了接收更快,优化
uint32_t start_upperBound,start_lowerBound;
uint32_t data_0_upperBound,data_1_upperBound,data_0_lowerBound,data_1_lowerBound;  

void LdrConfigInit()
{
	TIM5_Cap_Init(); //PA0接光敏传感器引脚,采用定时器输入捕获
	startSignal_time = 600; //起始信号高电平维持时间
	data_0_time = 800; //信号0高电平持续时间
	data_1_time = 1000; //信号1高电平持续时间
	upperAddValue = 100; //向上容许增加的时间范围
	lowerDecValue = 100; //向下容许减小的时间范围
	
	start_upperBound  = startSignal_time+upperAddValue;
	start_lowerBound  = startSignal_time-lowerDecValue;
	data_0_upperBound = data_0_time+upperAddValue;
	data_0_lowerBound = data_0_time-lowerDecValue;
	data_1_upperBound = data_1_time+upperAddValue;
	data_1_lowerBound = data_1_time-lowerDecValue;
}

uint16_t receiveData()
{
	uint32_t highVolTime = 0;
	short int dataBit = DATABIT;
	uint8_t dataFlag = 0;
	uint16_t data = 0;
	while(1)
	{
		if(TIM5CH1_CAPTURE_STA&0X80) //成功捕获到起始的信号
		{
			highVolTime = getHighVolTime();
			if((highVolTime<=start_upperBound) && (highVolTime>=start_lowerBound)) //第一次起始信号正确
			{
				while(1)
				{
					if(TIM5CH1_CAPTURE_STA&0X80) //数据信号
					{
						highVolTime = getHighVolTime();
						if((highVolTime>=data_0_lowerBound) && (highVolTime<=data_0_upperBound))
						{
							data &= ~(1<<(dataBit-1));
							dataBit--;
							if(dataBit==0)
							{
								dataFlag = 1;break;
							}
						}
						else if((highVolTime>=data_1_lowerBound) && (highVolTime<=data_1_upperBound))
						{
							data |= (1<<(dataBit-1));
							dataBit--;
							if(dataBit==0)
							{
								dataFlag = 1;break;
							}
						}
						else {printf("dataError:highVolTime:%d\r\n",highVolTime);break;}
					}
				}
				break;
			}
			else printf("startError:highVolTime:%d\r\n",highVolTime);
		}
	}
	if(dataFlag) return data;
	else return 0;
}

//配置接收相关的参数,
void configRecvParameter(uint32_t p1,uint32_t p2,uint32_t p3,uint32_t p4,uint32_t p5)
{
		startSignal_time = p1; //起始信号持续时间
		data_0_time = p2;  //信号0持续时间
	    data_1_time = p3;  //信号1持续时间
		upperAddValue = p4;  //往上增加范围
		lowerDecValue = p5; //往下减小的范围
	
		start_upperBound  = startSignal_time+upperAddValue;
		start_lowerBound  = startSignal_time-lowerDecValue;
		data_0_upperBound = data_0_time+upperAddValue;
		data_0_lowerBound = data_0_time-lowerDecValue;
		data_1_upperBound = data_1_time+upperAddValue;
		data_1_lowerBound = data_1_time-lowerDecValue;
}

void lookUpCurrentParameter() //查看当前发射参数
{
	printf("HoldTime:     start: %d  0信号: %d  1信号: %d\r\nWaitTime:    upper: %d  lower: %d\r\n", \
				startSignal_time,data_0_time,data_1_time, \
				upperAddValue,lowerDecValue);
}

  
【接收端 timer.c】

#include "timer.h"
#include "usart.h"

u8  TIM5CH1_CAPTURE_STA = 0;	//输入捕获状态		    				
u16	TIM5CH1_CAPTURE_VAL=0;	//输入捕获值

u8  TIM3_CountTime_STA = 0;	//计时状态
u8 counts = 0;   //计时溢出的次数
#define TIM3_ARR 0xFFFF 
#define TIM3_PSC 7199  //10Khz的计数频率,计数一次为100us

//通用定时器3中断初始化
//这里时钟选择为APB1的2倍,而APB1为36M
//arr:自动重装值。
//psc:时钟预分频数
//这里使用的是定时器3!
void TIM3_CountTime_Init() //最大计数65536
{
  TIM_TimeBaseInitTypeDef  TIM_TimeBaseStructure;
	NVIC_InitTypeDef NVIC_InitStructure;

	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); //时钟使能
	
	//定时器TIM3初始化
	TIM_TimeBaseStructure.TIM_Period = TIM3_ARR; //设置在下一个更新事件装入活动的自动重装载寄存器周期的值	
	TIM_TimeBaseStructure.TIM_Prescaler = TIM3_PSC; //设置用来作为TIMx时钟频率除数的预分频值
	TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; //设置时钟分割:TDTS = Tck_tim
	TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;  //TIM向上计数模式
	TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure); //根据指定的参数初始化TIMx的时间基数单位
 
	TIM_ITConfig(TIM3,TIM_IT_Update,ENABLE ); //使能指定的TIM3中断,允许更新中断

	//中断优先级NVIC设置
	NVIC_InitStructure.NVIC_IRQChannel = TIM3_IRQn;  //TIM3中断
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;  //先占优先级0级
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2;  //从优先级3级
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ通道被使能
	NVIC_Init(&NVIC_InitStructure);  //初始化NVIC寄存器
	
//	TIM_Cmd(TIM3, ENABLE);  //使能TIMx					 
}

//定时器3中断服务程序
void TIM3_IRQHandler(void)   //TIM3中断
{
	if (TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET)  //检查TIM3更新中断发生与否
	{ 
		TIM_ClearITPendingBit(TIM3, TIM_IT_Update);  //清除TIMx更新中断标志
		if((TIM3_CountTime_STA)==1) //还在计时
		{
			counts++;
		}
	}
}
//开始计时
void startCountRunningTime()
{
	TIM_Cmd(TIM3, ENABLE);
	TIM3_CountTime_STA = 1;
}
//返回代码运行计时时间
u32 getRunningTime()
{
	u32 times_us;
	times_us = ((counts*65536)+TIM3->CNT)*100;
	TIM3_CountTime_STA = 0;
	TIM3->CNT = 0;
	counts = 0;
	TIM_Cmd(TIM3, DISABLE);
	return times_us;
}
uint32_t getHighVolTime(void)
{
	uint32_t time_us = 0;
  time_us  = TIM5CH1_CAPTURE_STA&0X3F; //定时器溢出的次数
	if(time_us) time_us *= 65536;    //溢出时间总和
	time_us += TIM5CH1_CAPTURE_VAL;//得到总的高电平时间
	TIM5CH1_CAPTURE_STA = 0;//开启下一次捕获
	return time_us; 
}

#define TIM5_ARR 0xFFFF //最大65536us
#define TIM5_PSC 71  //1Mhz的计数频率,计数一次为1us

void TIM5_Cap_Init()
{	 
	//定时器5通道1输入捕获配置,用来读取高电平时间
	TIM_ICInitTypeDef  TIM5_ICInitStructure;
	GPIO_InitTypeDef GPIO_InitStructure;
	TIM_TimeBaseInitTypeDef  TIM_TimeBaseStructure;
  NVIC_InitTypeDef NVIC_InitStructure;

	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM5, ENABLE);	//使能TIM5时钟
 	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);  //使能GPIOA时钟
	
	GPIO_InitStructure.GPIO_Pin  = GPIO_Pin_0;  //PA0 清除之前设置  
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPD; //PA0 输入  
	GPIO_Init(GPIOA, &GPIO_InitStructure);
	GPIO_ResetBits(GPIOA,GPIO_Pin_0);						 //PA0 下拉
	
	//初始化定时器5 TIM5	 
	TIM_TimeBaseStructure.TIM_Period = TIM5_ARR; //设定计数器自动重装值 
	TIM_TimeBaseStructure.TIM_Prescaler = TIM5_PSC; 	//预分频器   
	TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; //设置时钟分割:TDTS = Tck_tim
	TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;  //TIM向上计数模式
	TIM_TimeBaseInit(TIM5, &TIM_TimeBaseStructure); //根据TIM_TimeBaseInitStruct中指定的参数初始化TIMx的时间基数单位
  
	//初始化TIM5输入捕获参数
	TIM5_ICInitStructure.TIM_Channel = TIM_Channel_1; //CC1S=01 	选择输入端 IC1映射到TI1上
  TIM5_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising;	//上升沿捕获
  TIM5_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI; //映射到TI1上
  TIM5_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1;	 //配置输入分频,不分频 
  TIM5_ICInitStructure.TIM_ICFilter = 0x00;//IC1F=0000 配置输入滤波器 不滤波
  TIM_ICInit(TIM5, &TIM5_ICInitStructure);
	
	//中断分组初始化
	NVIC_InitStructure.NVIC_IRQChannel = TIM5_IRQn;  //TIM3中断
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;  //先占优先级2级
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3;  //从优先级0级
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ通道被使能
	NVIC_Init(&NVIC_InitStructure);  //根据NVIC_InitStruct中指定的参数初始化外设NVIC寄存器 
	
	TIM_ITConfig(TIM5,TIM_IT_Update|TIM_IT_CC1,ENABLE);//允许更新中断 ,允许CC1IE捕获中断	
  TIM_Cmd(TIM5,ENABLE ); 	//使能定时器5
}
 
//定时器5中断服务程序	 
void TIM5_IRQHandler(void)
{ 
 	if((TIM5CH1_CAPTURE_STA&0X80)==0)//还未成功捕获	
	{	  
		if (TIM_GetITStatus(TIM5, TIM_IT_Update) != RESET)
		 
		{	    
			if(TIM5CH1_CAPTURE_STA&0X40)//已经捕获到高电平了
			{
				if((TIM5CH1_CAPTURE_STA&0X3F)==0X3F)//高电平太长了
				{
					TIM5CH1_CAPTURE_STA|=0X80;//标记成功捕获了一次
					TIM5CH1_CAPTURE_VAL=0XFFFF;
				}else TIM5CH1_CAPTURE_STA++;
			}	 
		}
	if (TIM_GetITStatus(TIM5, TIM_IT_CC1) != RESET)//捕获1发生捕获事件
		{	
			if(TIM5CH1_CAPTURE_STA&0X40)		//捕获到一个下降沿 		
			{	  			
				TIM5CH1_CAPTURE_STA|=0X80;		//标记成功捕获到一次高电平脉宽
				TIM5CH1_CAPTURE_VAL=TIM_GetCapture1(TIM5);
		   	    TIM_OC1PolarityConfig(TIM5,TIM_ICPolarity_Rising); //CC1P=0 设置为上升沿捕获
			}else  								//还未开始,第一次捕获上升沿
			{
				TIM5CH1_CAPTURE_STA=0;			//清空
				TIM5CH1_CAPTURE_VAL=0;
	 			TIM_SetCounter(TIM5,0);
				TIM5CH1_CAPTURE_STA|=0X40;		//标记捕获到了上升沿
		   	    TIM_OC1PolarityConfig(TIM5,TIM_ICPolarity_Falling);		//CC1P=1 设置为下降沿捕获
			}		    
		}			     	    					   
 	}
  TIM_ClearITPendingBit(TIM5, TIM_IT_CC1|TIM_IT_Update); //清除中断标志位
}

  
  
  
全部工程下载链接:
链接:https://pan.baidu.com/s/1r5pLaeCu5mieWKNu90GWDA
提取码:feng
跳转:可见光通信工程下载链接【提取码:feng】

  
  
如有错误,还望指正,谢谢!
  

猜你喜欢

转载自blog.csdn.net/fengge2018/article/details/108301334