目前课设已完成,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 afeng1、优化程序,优化部分变量,让程序运行的更快,尽量与接收端同步。* 2020.8.25 —— by afeng1、优化发射端串口助手,使其能够实时自动刷新电脑可用的串口,当该串口掉线,则会自动关闭连接。2、优化串口助手的显示区域部分,通过设置光标的位置使得文字可以从上至下显示。* 2020.8.23 —— by afeng1、加入连续发射多个数据的函数,并支持不同起始数据和结束数据,然后注册到USMART调试组件,以达到从串口助手来控制发射的数据。2、结合了自己的基于QT的串口助手,把本工程的USMART组件与该调试助手结合起来使用,更方便调参。* 2020.8.20 —— by afeng1、开始使用USMART调试组件,可以在串口助手通过函数带参数的方式在线修改各个参数,在后面不同距离下改变参数有着极大的便利性,免去了重复修改、编译和烧录代码的繁琐过程。2、解决了二进制数据位错误的BUG,是由于自己不够熟练位操作引起的,最后决定使用数字1的移位操作来提取出每个数据的各个二进制位。3、实现了基于滴答定时器的长时间延迟,由于该定时器只有16位,基于ST库写的延迟函数一次最大不能超过2s,经过额外编程实现能够一次延迟10s以上。4、剔除了每次数据都需要结束信号的方案,使得发射时间进一步提升,其实每个数据的起始信号也可以作为每个数据的区分了。* 2020.8.19 —— by afeng1、使用函数将十进制转二进制,并能够将每一个位通过亮-暗表示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); //串口初始化为115200delay_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 14uint32_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口速度为50MHzGPIO_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最大支持到1800msT._1s = (int)T._s; //1s的个数T._ms = (T._s-T._1s)*1000; //ms的数值,肯定小于1000for(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 afeng1、拓展使用外部SRAM,先定义好一个大数组作为缓冲,将所有接收到的数据存放起来,再进行误码验证,或者其他操作,减少了数据处理时间,让CPU能够更加专注去接收新的数据。2、使用TFTLCD用于显示状态信息及接收结果。* 2020.8.30 —— by afeng1、优化程序,优化部分变量,让程序运行的更快,尽量与发射端同步。* 2020.8.25 —— by afeng1、增加计时接收数据总用时的功能,使用定时器3,周期为10KHz,最长可计时超过 1 小时。2、增加错误数据统计功能,其思路为:接收一次数据前,先得到发送端即将发射数据块的起始数据、结束数据和总的长度,先接收三者并验证计算,如果正确则进入for循环接收,在for循环里的条件和接收端的一一致,因此在每次出现错误数据时,可以先记下错误的数据,直到下一次正确数据出现,再拿前后两个相减以调整循环的当前值并统计错误个数,达到与发射端同步的效果。即边接收边验证数据的正确。3、解决了当出现多次数据错误时统计错误个数的BUG,需要一个临时变量用来记录错误区间以调整当前循环值。* 2020.8.23 —— by afeng1、优化接收数据的判断方法,以前是当检测到引脚出现电平变化时,开始与接收端同样的延迟,延迟结束后若引脚电平出现如期的翻转,则认为该信号有效;现改为使用输入捕获的方式进行检验,当引脚电平变化,则会引起中断并开启定时器记录该引脚的高电平时间,直到引脚电平再次翻转,则可以读取出定时器的时间以判断信号,分别有起始信号、0 电平信号、1电平信号,分别采用不同高电平持续时间来判定。2、自己动手撸了一个基于QT的串口助手,结合本工程下的USMART调试组件可以达到通过串口助手很方便地在线调整发射与接受端的各个参数,分别有,发射端:起始信号、0信号、1信号的高电平时间,及其间隔低电平时间,接收端:起始信号、0信号、1信号的高电平判断时间,及其判断的上下限容许误差。此外,还可以指定发送数据的起始数据和结束数据。3、取消使用LCD显示接收的方案,因为显示一个数据太耗时间了。* 2020.8.20 —— by afeng1、开始使用USMART调试组件,可以在串口助手通过函数带参数的方式在线修改各个参数,在后面不同距离下改变参数有着极大的便利性,免去了重复修改、编译和烧录代码的繁琐过程。2、解决了偶尔出现数据连续接收错误的BUG,是因为在每次数据接收期间使用printf函数打印太多数据,导致接收端与发射端的时间失调不同步,即发射端发射起始信号时接收端还在处理printf函数,等待发射端发射数据时,接收端又将数据信号当作起始信号来判断,进而导致错误。* 2020.8.19 —— by afeng1、能够接收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); //串口初始化为115200NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //设置NVIC中断分组2:2位抢占优先级,2位响应优先级TIM3_CountTime_Init(); //用于计时接收数据的时间LdrConfigInit(); //初始化光敏传感器的引脚和输入捕获功能usmart_dev.init(SystemCoreClock/1000000); //初始化 USMART,实现在串口修改接收相关参数FSMC_SRAM_Init(); //初始化外部SRAMLCD_Init();memset(dataBuf,0,sizeof(dataBuf)); //全都初始化为0POINT_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 14extern 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_timTIM_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的计数频率,计数一次为1usvoid 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_timTIM_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】
如有错误,还望指正,谢谢!