Inspired by recently reading an article on Hack a Day about emulating Bluetooth LE with a cheap nRF24L01+ module, I decided to take the plung and order some off eBay. I ended up spending $11.96 for ten units with shipping included, so about $1.20 per unit. More than enough extras to have some fun
My initial thoughts were to use Dmitry’s example for bit-banging BTLE, but adapt it to run on an ATMega328-PU. This turned out to be a little more difficult than I expected mainly due to (I suspect) differences between the compiler Dmitry was using and AVR GCC. For example, Dmitry’s code makes use of the cbi
and abi
functions. I’m not familiar with these, and a few searches revealed that they’ve been missing (deprecated then removed actually) from AVR GCC for some time. Not fully understanding the purpose of those functions either I was handicapped in my porting efforts.
I eventually found a port of this sample to Arduino and from that I was able to infer what should be done. Eventually, I ended up with this:
#include <stdio.h> | |
#include <avr/io.h> | |
#include <avr/interrupt.h> | |
#include <avr/sleep.h> | |
#include <util/delay.h> | |
#define PIN_CE 1 //Output | |
#define PIN_nCS 2 //Output | |
#define output_low(port,pin) port &= ~(1<<pin) | |
#define output_high(port,pin) port |= (1<<pin) | |
#define set_input(portdir,pin) portdir &= ~(1<<pin) | |
#define set_output(portdir,pin) portdir |= (1<<pin) | |
#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_t 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]); | |
} | |
void SPI_init(void){ | |
DDRB = ((1<<DDB2)|(1<<DDB1)|(1<<DDB0)); //spi pins on port b MOSI SCK,SS outputs | |
SPCR = ((1<<SPE)|(1<<MSTR)|(1<<SPR0)|(1<<CPOL)|(1<<CPHA)); // SPI enable, Master, f/16 | |
} | |
uint8_t spi_byte(uint8_t byte){ | |
SPDR = byte; | |
while(!(SPSR & (1<<SPIF))) | |
; | |
return SPDR; | |
} | |
void nrf_cmd(uint8_t cmd, uint8_t data){ | |
output_low(PORTB, PIN_nCS); | |
spi_byte(cmd); | |
spi_byte(data); | |
output_high(PORTB, PIN_nCS); | |
} | |
void nrf_simplebyte(uint8_t cmd){ | |
output_low(PORTB, PIN_nCS); | |
spi_byte(cmd); | |
output_high(PORTB, PIN_nCS); | |
} | |
void nrf_manybytes(uint8_t* data, uint8_t len){ | |
output_low(PORTB, PIN_nCS); | |
do{ | |
spi_byte(*data++); | |
}while(--len); | |
output_high(PORTB, PIN_nCS); | |
} | |
void fob_init (void){ | |
//DDRA = (uint8_t)~(1<<5); | |
DDRB = 0b00000110; | |
//PORTA = 0b10001111; | |
output_low(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(); | |
SPI_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++] = 0x42; //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 | |
output_low(PORTB, PIN_nCS); | |
spi_byte(0xA0); | |
for(i = 0 ; i < L ; i++) spi_byte(buf[i]); | |
output_high(PORTB, PIN_nCS); | |
nrf_cmd(0x20, 0x12); //tx on | |
output_high(PORTB, PIN_CE); | |
_delay_ms(10); | |
output_low(PORTB, PIN_CE); //(in preparation of switching to RX quickly) | |
} | |
return 0; | |
} |
Unfortunately this doesn’t seem to do anything. At least, not according to iStumbler – running their Bluetooth scanner didn’t reveal any signs that my little AVR was broadcasting.
This was about as far as I was able to make it over the weekend, so I don’t yet know what the problem is. I suspect that either I’m using iStumbler wrong, or there’s something wrong with the way I ported the SPI implementation.
Either way, that’s something I’ll have to dig into later.