Faking Bluetooth LE

"Bit-Banging" Bluetooth Low Energy - Dmitry.GR

Lijun // Using nRF24L01+ as A Bluetooth Low Energy Broadcaster/Beacon

Bluetooth LE is a new technology, introduced in the Bluetooth 4.0 spec. It has absolutely nothing to do with bluetooth besides the name. Now that we have that out of the way, why is it cool? Well, it was made for low power, and the design shows. Unlike real bluetooth that does frequency hoppping on a precise schedule, regardless of anything, LE hops after some number of packets are sent, and thus one does not need to be awake to keep a running clock to know where to hop to next. In fact, LE allows a device to completely shut down its radio for large periods of time while maintaining a connection. This makes it awesome for keyboards and mice and all kinds of other such things. Another cool feature in LE is that devices can send unsolicited broadcasts of small chunks of data. Unlike real bluetooth, scanning for devices in the LE world can be passive - you just listen for advertisement packets on the right channels and you hear all the advertisements.

The simple channel hopping behaviour of LE means that we can probably pretend to be an LE device without the complex radio that normal bluetoth requires. The frequency is 2.4GHz, chanels are 1MHz apart, modulation is GFSK, datarate is 1MBps, preamble is 10101010 or 01010101 based on first data byte, and addressing is done using a 32-bit address. Gee, don't we know a device that can do all that? Of course we do! The ever-popular Nordic nRF24L01+. So let's look at the differences between what LE needs and what we have:

  • CRC: BTLE uses a 24-bit CRC that is not something nRF24 can do. Luckily for us, nRF24 allows us to disable the CRC, and then we can send our own and check received ones manually. Unpleasant? Sure, but doable.
  • Frequency Hopping: BTLE needs ut to be able to hop quickly - 150us or faster. Sadly here nRF24 fails us. After every transmission it shuts down its PLL, and then restarts it on every transmit or receive command. This takes 130us. We are left with too little time to do anything here, sadly. Luckily this is not the major issue.
  • Packet Length: BTLE packets are of various sizes, up to 39 bytes of payload in fact. But the one packet we would really want to support is the CONNECT_REQ packet, which is in fact 34 bytes of payload + 3 of CRC, meaning we'd need to receive 37 bytes if we want to check CRCs, and even if we were willing to throw caution to the wind and ignore them, we need to receive 34. nRF24 will not handle packets over 32 bytes. Sadly the last two bytes are kind of important (hop size and a part of the channel map). This is where we lose.

So... we cannot accept connections, but not all is lost... BTLE allows unsolicited advertisements, so we can still do some cool things by making them broadcast data to anyone who'll listen. Let's work out just how much data we can send... Out of our 32-byte budget: 3 go into CRC, 2 go into the ADV_NONCONN_IND packet header, and 6 go into the MAC address, leaving us with 21 bytes of payload. This payload, however, must have structure. Assuming minimum required headers, we can get away with sending 19 bytes of data if we do not want our device to broadcast a name. If we do want a name, we have 17 bytes to split between the name (in UTF-8) and our data. And if we want to comply with the spec better and broadcast device attributes, we'll have 14 bytes to split between name and data or 16 bytes of pure data - not too bad.

Let's sort out the details then... first of all, BTLE and nRF24 send data bits in the air in opposite order, so we'll have to reverse all our bits. Second, BTLE uses data whitening, and nRF24 does not, so we'll need to do that by hand too. Lastly, there is the previously-mentioned 24-bit CRC. All LE broadcasts get sent to the same "address": 0x8E89BED6, also known as "bed six." Of course, for us it'll be bit-reversed. BTLE applies CRC to the whole payload but not the address. Whitening is applied to the payload and the CRC. Knowing this, thus, gives us the ordering of events needed to assemble a complete working packet. Advertisement packets are sent on 3 channels: 37, 38, and 39, which are 2.402HGz, 2.426GHz, and 2.480GHz respectively. We'll alternate between them, spewing our broadcast everywhere methodically.

BTLE CRC is not too hard to implement in C, uses the initial value of 0x555555, and looks something like this:

void btLeCrc(const uint8_t* data, uint8_t len, uint8_t* dst){

	uint8_t v, t, d;

	while(len--){
		d = *data++;
		for(v = 0; v < 8; v++, d >>= 1){
		
			t = dst[0] >> 7;
			dst[0] <<= 1;
			if(dst[1] & 0x80) dst[0] |= 1;
			dst[1] <<= 1;
			if(dst[2] & 0x80) dst[1] |= 1;
			dst[2] <<= 1;
			
			if(t != (d & 1)){
			
				dst[2] ^= 0x5B;
				dst[1] ^= 0x06;
			}
		}	
	}
}

The data whitening function is also not too complicated. It is a 7-bit linear-shift feedback style and is initialized by the value that is equal to (channelNum << 1) + 1. The code goes something like this:

void btLeWhiten(uint8_t* data, uint8_t len, uint8_t whitenCoeff){
	uint8_t  m;
	
	while(len--){
	
		for(m = 1; m; m <<= 1){
		
			if(whitenCoeff & 0x80){
				
				whitenCoeff ^= 0x11;
				(*data) ^= m;
			}
			whitenCoeff <<= 1;
		}
		data++;
	}
}

The advertisement packet payload looks like this:

struct adv_hdr{
	uint8_t header; //we use 0x40 to say it is a non-connectable undirected
			//advertisement and address we're sending is random (not assigned)
	uint8_t dataLen; //length of following data (including MAC addr)
	uint8_t MAC[6]; //the mac address
}

So if we lump all this together, we'll end up with a packet with the above header. We then CRC it using the above CRC function. We then whiten it using the above whitening function. After this we send it. Let's see... yup, it works. An iPad3 running BTLExplorer shows that our device is visible and is a BTLE device. Cool! One caveat: if you do not broadcast a device name, BTLExplorer will crash - this is their bug, so do not worry.

What is the payload data format? you may ask. Well, data is made of chunks, each of which has a 2-byte header: length and type (in that order, length includes the length of the type byte). Types you care about are:

  • 2: Flags - length will be 2 and the lone byte's value you want is 5 - this means a single-mode (not BTLE/BT combo) device in limited discovery mode.
  • 8 or 9: Name - length is the length of the name in UTF-8 without NULL-termination. 8 is for "shortened name" and 9 is for "complete name."
  • 255: Custom data. This is where you can shove your custom data. On iPhone you'll have complete access to this.

Why all this? Well, if you know a simpler way for an embedded project to communicate data to an iPhone, let me know. WiFi is power hungry and messy. Real bluetooth is locked down in iPhone, as are serial ports. This method works. If and when Android gets a BTLE API, I am sure broadcast data will be available to you too - it just makes sense.

Future work: I just got some non-nordic 2.4GHz parts that support packets up to 64 bytes and can keep their PPLs on, meaning that a full BTLE stack may be possible on them. I am working on this as you read this.

All the code as well as the research that went into this and is published here is under this license: you may use it in any way you please if and only if it is for non-commercial purposes, you must provide a link to this page as well. Any commercial use must be discussed with me.

Full sample code, that will run on the nordic-fob from Sparkfun looks approximately like this:

#include <stdio.h>
#include <avr/io.h>
#include <avr/interrupt.h>
#include <avr/sleep.h>
#define F_CPU	8000000
#include <avr/delay.h>


#define PIN_CE	1 //Output
#define PIN_nCS	2 //Output


#define MY_MAC_0	0xEF
#define MY_MAC_1	0xFF
#define MY_MAC_2	0xC0
#define MY_MAC_3	0xAA
#define MY_MAC_4	0x18
#define MY_MAC_5	0x00


ISR(PCINT0_vect)
{
	//useless
}

void btLeCrc(const uint8_t* data, uint8_t len, uint8_t* dst){

	uint8_t v, t, d;

	while(len--){
	
		d = *data++;
		for(v = 0; v < 8; v++, d >>= 1){
		
			t = dst[0] >> 7;
			
			dst[0] <<= 1;
			if(dst[1] & 0x80) dst[0] |= 1;
			dst[1] <<= 1;
			if(dst[2] & 0x80) dst[1] |= 1;
			dst[2] <<= 1;
			
		
			if(t != (d & 1)){
			
				dst[2] ^= 0x5B;
				dst[1] ^= 0x06;
			}
		}	
	}
}

uint8_t  swapbits(uint8_t a){

	uint8 v = 0;
	
	if(a & 0x80) v |= 0x01;
	if(a & 0x40) v |= 0x02;
	if(a & 0x20) v |= 0x04;
	if(a & 0x10) v |= 0x08;
	if(a & 0x08) v |= 0x10;
	if(a & 0x04) v |= 0x20;
	if(a & 0x02) v |= 0x40;
	if(a & 0x01) v |= 0x80;

	return v;
}

void btLeWhiten(uint8_t* data, uint8_t len, uint8_t whitenCoeff){

	uint8_t  m;
	
	while(len--){
	
		for(m = 1; m; m <<= 1){
		
			if(whitenCoeff & 0x80){
				
				whitenCoeff ^= 0x11;
				(*data) ^= m;
			}
			whitenCoeff <<= 1;
		}
		data++;
	}
}

static inline uint8_t btLeWhitenStart(uint8_t chan){
	//the value we actually use is what BT'd use left shifted one...makes our life easier

	return swapbits(chan) | 2;	
}

void btLePacketEncode(uint8_t* packet, uint8_t len, uint8_t chan){
	//length is of packet, including crc. pre-populate crc in packet with initial crc value!

	uint8_t i, dataLen = len - 3;
	
	btLeCrc(packet, dataLen, packet + dataLen);
	for(i = 0; i < 3; i++, dataLen++) packet[dataLen] = swapbits(packet[dataLen]);
	btLeWhiten(packet, len, btLeWhitenStart(chan));
	for(i = 0; i < len; i++) packet[i] = swapbits(packet[i]);
	
}

uint8_t spi_byte(uint8_t byte){

	uint8_t i = 0;
	
	do{
		PORTB &=~ (uint8_t)(1 << 6);
		if(byte & 0x80) PORTB |= (uint8_t)(1 << 6);
		CLK |= (uint8_t)(1 << 4);
		byte <<= 1;
		if(PINA & (uint8_t)32) byte++;
		CLK &=~ (uint8_t)(1 << 4);
	
	}while(--i);

	return byte;
}

void nrf_cmd(uint8_t cmd, uint8_t data)
{
	cbi(PORTB, PIN_nCS);
	spi_byte(cmd);
	spi_byte(data);
	sbi(PORTB, PIN_nCS); //Deselect chip
}

void nrf_simplebyte(uint8_t cmd)
{
	cbi(PORTB, PIN_nCS);
	spi_byte(cmd);
	sbi(PORTB, PIN_nCS);
}

void nrf_manybytes(uint8_t* data, uint8_t len){

	cbi(PORTB, PIN_nCS);
	do{
	
		spi_byte(*data++);
	
	}while(--len);
	sbi(PORTB, PIN_nCS);
}

void fob_init (void)
{
	DDRA = (uint8_t)~(1<<5);
	DDRB = 0b00000110;
	PORTA = 0b10001111;
	cbi(PORTB, PIN_CE);
	TCCR0B = (1<<CS00);
	MCUCR = (1<<SM1)|(1<<SE);
	sei();
}

int main (void)
{
	static const uint8_t chRf[] = {2, 26,80};
	static const uint8_t chLe[] = {37,38,39};
	uint8_t i, L, ch = 0;
	uint8_t buf[32];
	
	fob_init();
	
	DDRA |= 4;
	PORTA |= 4;
	
	nrf_cmd(0x20, 0x12);	//on, no crc, int on RX/TX done
	nrf_cmd(0x21, 0x00);	//no auto-acknowledge
	nrf_cmd(0x22, 0x00);	//no RX
	nrf_cmd(0x23, 0x02);	//5-byte address
	nrf_cmd(0x24, 0x00);	//no auto-retransmit
	nrf_cmd(0x26, 0x06);	//1MBps at 0dBm
	nrf_cmd(0x27, 0x3E);	//clear various flags
	nrf_cmd(0x3C, 0x00);	//no dynamic payloads
	nrf_cmd(0x3D, 0x00);	//no features
	nrf_cmd(0x31, 32);	//always RX 32 bytes
	nrf_cmd(0x22, 0x01);	//RX on pipe 0
	
	buf[0] = 0x30;			//set addresses
	buf[1] = swapbits(0x8E);
	buf[2] = swapbits(0x89);
	buf[3] = swapbits(0xBE);
	buf[4] = swapbits(0xD6);
	nrf_manybytes(buf, 5);
	buf[0] = 0x2A;
	nrf_manybytes(buf, 5);

	
	
	while(1){
		
		L = 0;
		
		buf[L++] = 0x40;	//PDU type, given address is random
        buf[L++] = 11;      //17 bytes of payload
		
		buf[L++] = MY_MAC_0;
		buf[L++] = MY_MAC_1;
		buf[L++] = MY_MAC_2;
		buf[L++] = MY_MAC_3;
		buf[L++] = MY_MAC_4;
		buf[L++] = MY_MAC_5;
		
		buf[L++] = 2;		//flags (LE-only, limited discovery mode)
		buf[L++] = 0x01;
		buf[L++] = 0x05;
		
		buf[L++] = 7;
		buf[L++] = 0x08;
		buf[L++] = 'n';
		buf[L++] = 'R';
		buf[L++] = 'F';
		buf[L++] = ' ';
		buf[L++] = 'L';
		buf[L++] = 'E';
		
		buf[L++] = 0x55;	//CRC start value: 0x555555
		buf[L++] = 0x55;
		buf[L++] = 0x55;
		
		
		if(++ch == sizeof(chRf)) ch = 0;
		
		nrf_cmd(0x25, chRf[ch]);
		nrf_cmd(0x27, 0x6E);	//clear flags

		btLePacketEncode(buf, L, chLe[ch]);
		
		nrf_simplebyte(0xE2); //Clear RX Fifo
		nrf_simplebyte(0xE1); //Clear TX Fifo
	
		cbi(PORTB, PIN_nCS);
		spi_byte(0xA0);
		for(i = 0 ; i < L ; i++) spi_byte(buf[i]);
		sbi(PORTB, PIN_nCS);
	
		nrf_cmd(0x20, 0x12);	//tx on
		sbi(PORTB, PIN_CE);	 //do tx
		delay_ms(10);
		cbi(PORTB, PIN_CE);	(in preparation of switching to RX quickly)
	}
	
	
	return 0;
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
使用 JavaScript 编写的记忆游戏(附源代码)   项目:JavaScript 记忆游戏(附源代码) 记忆检查游戏是一个使用 HTML5、CSS 和 JavaScript 开发的简单项目。这个游戏是关于测试你的短期 记忆技能。玩这个游戏 时,一系列图像会出现在一个盒子形状的区域中 。玩家必须找到两个相同的图像并单击它们以使它们消失。 如何运行游戏? 记忆游戏项目仅包含 HTML、CSS 和 JavaScript。谈到此游戏的功能,用户必须单击两个相同的图像才能使它们消失。 点击卡片或按下键盘键,通过 2 乘 2 旋转来重建鸟儿对,并发现隐藏在下面的图像! 如果翻开的牌面相同(一对),您就赢了,并且该对牌将从游戏中消失! 否则,卡片会自动翻面朝下,您需要重新尝试! 该游戏包含大量的 javascript 以确保游戏正常运行。 如何运行该项目? 要运行此游戏,您不需要任何类型的本地服务器,但需要浏览器。我们建议您使用现代浏览器,如 Google Chrome 和 Mozilla Firefox, 以获得更好、更优化的游戏体验。要玩游戏,首先,通过单击 memorygame-index.html 文件在浏览器中打开游戏。 演示: 该项目为国外大神项目,可以作为毕业设计的项目,也可以作为大作业项目,不用担心代码重复,设计重复等,如果需要对项目进行修改,需要具备一定基础知识。 注意:如果装有360等杀毒软件,可能会出现误报的情况,源码本身并无病毒,使用源码时可以关闭360,或者添加信任。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值