——LLCC68,基本编程方法
作者:fujian
基本电路
使用 Ra-01SC 模块+AVR单片机 搭建基本应用电路,实现串口(RS422)透传功能。
(对于Ra-01SC 模块,淘宝商家给的示例代码,是用结构体+指针模仿C++的类对象来实现,太绕了,不直观,比较难理解。)
原本想用两块Lora模块实现全双工,但由于两个模块在电路板上相距不够远,会互相产生干扰,所以还是半双工吧,只是一块用于发送,另一块用于接收。
Lora 模块 对接 AVR 硬件SPI口,AVR通过串口转成RS422,也预留了RS485接口。 为啥不用STM32,而用AVR呢,因为手头正好还有好几块这个芯片。
用Kicad7.0 画了板
嘉立创打样,自己焊了两块板。
LLCC68 官方驱动:GitHub - Lora-net/llcc68_driver: Driver for LLCC68 radio
驱动说明原文
llcc68.c 驱动函数,包含一些说明。
llcc68.h 是llcc68.c的头文件,一些宏定义,枚举类型,驱动函数声明等。
llcc68_regs.h 芯片所有的寄存器声明。
llcc68_hal.h 硬件层函数的头文件,里面声明好了需要我们编写的硬件层函数。
我们需要新建个llcc68_hal.c,把 llcc68_hal_reset(),llcc68_hal_wakeup(),llcc68_hal_write(),llcc68_hal_read() 4个硬件层函数完成。
llcc68_hal_reset() —— llcc68硬件复位函数
llcc68_hal_wakeup() —— llcc68唤醒函数
llcc68_hal_write() ——MCU SPI写llcc68函数
llcc68_hal_read()——MCU SPI读llcc68函数
完成以上几个硬件层函数后,便可以直接调用llcc68.c里的函数对llcc68进行操作。
我把IO口初始化也写在llcc68_hal.c里了,
以下是我的llcc68_hal.h和llcc68_hal.c代码,水平有限~ 多多包含~
#ifndef LLCC68_HAL_H
#define LLCC68_HAL_H
#ifdef __cplusplus
extern "C" {
#endif
/*
* -----------------------------------------------------------------------------
* --- DEPENDENCIES ------------------------------------------------------------
*/
#include <stdint.h>
#include <stdbool.h>
#include <avr/io.h>
/*
* -----------------------------------------------------------------------------
* --- PUBLIC MACROS -----------------------------------------------------------
*/
/*
* -----------------------------------------------------------------------------
* --- PUBLIC CONSTANTS --------------------------------------------------------
*/
/**
* @brief Write this to SPI bus while reading data, or as a dummy/placeholder
*/
#define LLCC68_NOP ( 0x00 )
#define SET_LLCC68_0_nRESET() PORTA|=(1<<PORTA0)
#define CLR_LLCC68_0_nRESET() PORTA&=~(1<<PORTA0)
#define SET_LLCC68_1_nRESET() PORTA|=(1<<PORTA2)
#define CLR_LLCC68_1_nRESET() PORTA&=~(1<<PORTA2)
#define SET_LLCC68_0_NSS() PORTB|=(1<<PORTB4)
#define CLR_LLCC68_0_NSS() PORTB&=~(1<<PORTB4)
#define SET_LLCC68_1_NSS() PORTA|=(1<<PORTA4)
#define CLR_LLCC68_1_NSS() PORTA&=~(1<<PORTA4)
#define LLCC68_0_command_busy PINA&0x02
#define LLCC68_1_command_busy PINA&0x08
/*
* -----------------------------------------------------------------------------
* --- PUBLIC TYPES ------------------------------------------------------------
*/
typedef enum llcc68_hal_status_e
{
LLCC68_HAL_STATUS_OK = 0,
LLCC68_HAL_STATUS_ERROR = 3,
} llcc68_hal_status_t;
/*
* -----------------------------------------------------------------------------
* --- PUBLIC FUNCTIONS PROTOTYPES ---------------------------------------------
*/
llcc68_hal_status_t llcc68_interface_init();
/**
* Radio data transfer - write
*
* @remark Shall be implemented by the user
*
* @param [in] context Radio implementation parameters
* @param [in] command Pointer to the buffer to be transmitted
* @param [in] command_length Buffer size to be transmitted
* @param [in] data Pointer to the buffer to be transmitted
* @param [in] data_length Buffer size to be transmitted
*
* @returns Operation status
*/
llcc68_hal_status_t llcc68_hal_write( const void* context, const uint8_t* command, const uint16_t command_length,
const uint8_t* data, const uint16_t data_length );
/**
* Radio data transfer - read
*
* @remark Shall be implemented by the user
*
* @param [in] context Radio implementation parameters
* @param [in] command Pointer to the buffer to be transmitted
* @param [in] command_length Buffer size to be transmitted
* @param [in] data Pointer to the buffer to be received
* @param [in] data_length Buffer size to be received
*
* @returns Operation status
*/
llcc68_hal_status_t llcc68_hal_read( const void* context, const uint8_t* command, const uint16_t command_length,
uint8_t* data, const uint16_t data_length );
/**
* Reset the radio
*
* @remark Shall be implemented by the user
*
* @param [in] context Radio implementation parameters
*
* @returns Operation status
*/
llcc68_hal_status_t llcc68_hal_reset( const void* context );
/**
* Wake the radio up.
*
* @remark Shall be implemented by the user
*
* @param [in] context Radio implementation parameters
*
* @returns Operation status
*/
llcc68_hal_status_t llcc68_hal_wakeup( const void* context );
#ifdef __cplusplus
}
#endif
#endif // LLCC68_HAL_H
/* --- EOF ------------------------------------------------------------------ */
#include "llcc68_hal.h"
#include <util/delay.h>
#include "avr_spi.h"
llcc68_hal_status_t llcc68_interface_init() //IO口初始化
{
DDRA=0x15;
PORTA=0x1F;
DDRB=0xB0;
PORTB=0x10;
DDRC=0x30;
PORTC=0x20;
DDRD=0x00;
PORTD=0xff;
return LLCC68_HAL_STATUS_OK;
}
llcc68_hal_status_t llcc68_hal_reset( const void* context ) //LLCC68 硬件复位函数
{
uint8_t which_one_llcc68=0;
const uint8_t *p;
p=context;
which_one_llcc68 = *p;
if (which_one_llcc68==0) {CLR_LLCC68_0_nRESET();}
else {CLR_LLCC68_1_nRESET();}
_delay_us(150);
SET_LLCC68_0_nRESET();
SET_LLCC68_1_nRESET();
_delay_us(3500);
return LLCC68_HAL_STATUS_OK;
}
llcc68_hal_status_t llcc68_hal_write( const void* context, const uint8_t* command, const uint16_t command_length,
const uint8_t* data, const uint16_t data_length ) // 写LLCC68函数
{
uint8_t i=0;
uint16_t counter=0;
uint8_t which_one_llcc68=0;
const uint8_t *p;
p=context;
which_one_llcc68 = *p;
if(which_one_llcc68==0)
{
while (LLCC68_0_command_busy)
{
counter++;
if (counter>2000)
{
return LLCC68_HAL_STATUS_ERROR;
}
}
}
else
{
while (LLCC68_1_command_busy)
{
counter++;
if (counter>2000)
{
return LLCC68_HAL_STATUS_ERROR;
}
}
}
if (which_one_llcc68==0){CLR_LLCC68_0_NSS();}
else {CLR_LLCC68_1_NSS();}
_delay_us(10);
for (i=0;i<command_length;i++)
{
SPI_Write_Read_Byte(command[i]);
}
for (i=0;i<data_length;i++)
{
SPI_Write_Read_Byte(data[i]);
}
if (which_one_llcc68==0){SET_LLCC68_0_NSS();}
else {SET_LLCC68_1_NSS();}
return LLCC68_HAL_STATUS_OK;
}
llcc68_hal_status_t llcc68_hal_read( const void* context, const uint8_t* command, const uint16_t command_length,
uint8_t* data, const uint16_t data_length ) //读取LLCC68
{
uint8_t i=0;
uint16_t counter=0;
uint8_t which_one_llcc68=0;
const uint8_t *p;
p=context;
which_one_llcc68 = *p;
if(which_one_llcc68==0)
{
while (LLCC68_0_command_busy)
{
counter++;
if (counter>2000)
{
return LLCC68_HAL_STATUS_ERROR;
}
}
}
else
{
while (LLCC68_1_command_busy)
{
counter++;
if (counter>2000)
{
return LLCC68_HAL_STATUS_ERROR;
}
}
}
if (which_one_llcc68==0){CLR_LLCC68_0_NSS();}
else {CLR_LLCC68_1_NSS();}
_delay_us(10);
for (i=0;i<command_length;i++)
{
SPI_Write_Read_Byte(command[i]);
}
for (i=0;i<data_length;i++)
{
data[i]=SPI_Write_Read_Byte(0);
}
if (which_one_llcc68==0){SET_LLCC68_0_NSS();}
else {SET_LLCC68_1_NSS();}
return LLCC68_HAL_STATUS_OK;
}
llcc68_hal_status_t llcc68_hal_wakeup( const void* context ) //唤醒LLCC68
{
uint16_t counter=0;
uint8_t which_one_llcc68=0;
const uint8_t *p;
p=context;
which_one_llcc68 = *p;
if (which_one_llcc68==0){CLR_LLCC68_0_NSS();}
else {CLR_LLCC68_1_NSS();}
_delay_us(10);
if (which_one_llcc68==0){SET_LLCC68_0_NSS();}
else {SET_LLCC68_1_NSS();}
_delay_us(4000);
if(which_one_llcc68==0)
{
while (LLCC68_0_command_busy)
{
counter++;
if (counter>2000)
{
return LLCC68_HAL_STATUS_ERROR;
}
}
}
else
{
while (LLCC68_1_command_busy)
{
counter++;
if (counter>2000)
{
return LLCC68_HAL_STATUS_ERROR;
}
}
}
return LLCC68_HAL_STATUS_OK;
}
写完硬件层代码后,根据数据手册基本发送操作写代码。
截取数据手册基本发送操作步骤:
但要注意,这里缺少了 设置DIO2为自动切换天线的代码,编程的时候注意写上,不然发送的时候天线连接的还是接收电路。
截取数据手册基本接收操作步骤:
参照这些基本步骤,稍作修改,
编程思路为:
写个接收初始化函数,让接收机一直处于接收状态,这里基本收发操作,先不考虑功耗。当有数据收到产生中断后,通知接收函数处理数据。
写个发送初始化函数,初始化大部分参数,另写一个发送函数,用于发送数据。
写个中断处理函数,用于处理收发数据事宜 。
代码如下(radio.h和radio.c):
#ifndef RADIO_H_
#define RADIO_H_
#include "llcc68.h"
#include "llcc68_hal.h"
#include "llcc68_regs.h"
#include <stdio.h>
extern uint8_t llcc68_which_0;
extern uint8_t llcc68_which_1;
extern uint8_t radio_irq_flag;
extern llcc68_pkt_status_lora_t last_pkt_status;
extern llcc68_rx_buffer_status_t last_rxbuff_status;
extern uint8_t rx_buff[256];
void radiorx_init(const void* context);
void radiotx_init(const void* context);
void radiosend(const void* context, uint8_t *playload,uint8_t length);
void radio_irq_processing(const void *context);
void radio_0_irq(void);
void radio_1_irq(void);
void radioreceive(const void* context, uint8_t *rxbuff);
void clear_rxbuff(void);
#endif /* RADIO_H_ */
#include "radio.h"
#define N0 //条件编译,N0是第一块板,N1是第二块板 两块板配对使用。
uint8_t llcc68_which_0 =0;
uint8_t llcc68_which_1 =1;
uint8_t buff[256]={0};
#ifdef N0
llcc68_mod_params_lora_t rx_lora_par={LLCC68_LORA_SF9,LLCC68_LORA_BW_125,LLCC68_LORA_CR_4_5,0}; //定义接收Lora调制器相关参数,扩频因子(SF),带宽(BW),编码率(CR),低速优化(LDRO)
llcc68_mod_params_lora_t tx_lora_par={LLCC68_LORA_SF9,LLCC68_LORA_BW_125,LLCC68_LORA_CR_4_5,0}; //定义发送Lora调制器相关参数,扩频因子(SF),带宽(BW),编码率(CR),低速优化(LDRO)
#endif
#ifdef N1
llcc68_mod_params_lora_t rx_lora_par={LLCC68_LORA_SF9,LLCC68_LORA_BW_125,LLCC68_LORA_CR_4_5,0}; //同上
llcc68_mod_params_lora_t tx_lora_par={LLCC68_LORA_SF9,LLCC68_LORA_BW_125,LLCC68_LORA_CR_4_5,0}; //同上
#endif
llcc68_pkt_params_lora_t rx_pkt_par={8,LLCC68_LORA_PKT_EXPLICIT,255,true,false}; //定义接收Lora包参数,前导码长度,头类型,PayLoad长度,CRC校验是否开启,IRQ极性是否反转
llcc68_pkt_params_lora_t tx_pkt_par={8,LLCC68_LORA_PKT_EXPLICIT,255,true,false}; //定义发送Lora包参数,前导码长度,头类型,PayLoad长度,CRC校验是否开启,IRQ极性是否反转
llcc68_pa_cfg_params_t pa_cfg={0x04,0x07,0x0,0x01}; //定义功率放大器参数,分别为:paDutyCycle,hpMax, deviceSel, paLut 可参考数据手册71页末的表格
#ifdef N0
uint32_t rx_freq = 470300000; //定义发送频率
uint32_t tx_freq = 509700000; //定义接收频率
#endif
#ifdef N1
uint32_t rx_freq = 509700000; //同上
uint32_t tx_freq = 470300000; //同上
#endif
uint8_t radio_irq_flag=0; //定义一个标志变量,当INT0产生中断时,置bit0为1,当INT1产生中断时,置bit1为1。
llcc68_pkt_status_lora_t last_pkt_status; //定义PKT状态变量,用于保存上一包数据的RSSI,Snr,和解扩后的RSSI。
llcc68_rx_buffer_status_t last_rxbuff_status; //定义接收缓存状态,用于保存上一次收到数据的状态,包含接收到的字节数量和缓存起始指针
uint8_t rx_buff[256]={0}; //定义接收缓存数组
void radiorx_init(const void *context) //接收初始化函数,参照数据手册基本接收操作编写
{
#ifdef N0
uint8_t sync_word[2]={0x14,0x24}; //定义同步字
#endif
#ifdef N1
uint8_t sync_word[2]={0x14,0x24}; //定义同步字
#endif
llcc68_hal_reset(context); //复位接收机
llcc68_set_standby(context, LLCC68_STANDBY_CFG_RC); //接收机转到待机状态
llcc68_set_pkt_type(context, LLCC68_PKT_TYPE_LORA); //设置接收机数据包类型为LoRA
llcc68_set_rf_freq(context, rx_freq); //设置接收机频率
llcc68_set_reg_mode(context,LLCC68_REG_MODE_DCDC); //设置启用接收机的DC-DC
llcc68_set_buffer_base_address(context,0xff,0x00); //设置接收机发送与接收缓存指针
llcc68_set_lora_mod_params(context, &rx_lora_par); //设置Lora调制参数
llcc68_set_lora_pkt_params(context, &rx_pkt_par); //设置Lora数据包参数
llcc68_set_dio_irq_params(context,0x42,0x42,0x00,0x00); //设置接收机中断相关参数,参见数据手册74页。
llcc68_write_register(context,0x0740,sync_word,2); //设置同步字符
llcc68_set_rx_with_timeout_in_rtc_step( context,LLCC68_RX_CONTINUOUS );//接收机转换到连续接收状态
}
void radiotx_init(const void* context) //发送初始化函数,参照数据手册基本发送操作编写
{
#ifdef N0
uint8_t sync_word[2]={0x14,0x24};
#endif
#ifdef N1
uint8_t sync_word[2]={0x14,0x24};
#endif
llcc68_hal_reset(context); //复位发送机
llcc68_set_standby(context,LLCC68_STANDBY_CFG_RC); //发送机转到待机状态
llcc68_set_pkt_type(context,LLCC68_PKT_TYPE_LORA); //设置接收机数据包类型为LoRA
llcc68_set_rf_freq(context,tx_freq); //设置发送机频率
llcc68_set_dio2_as_rf_sw_ctrl(context,true); //设置DIO2自动切换天线
llcc68_set_reg_mode( context,LLCC68_REG_MODE_DCDC); //设置启用发送机的DC-DC
llcc68_set_pa_cfg(context,&pa_cfg); //设置功率放大器(PA)参数
llcc68_set_tx_params(context, 0x16,LLCC68_RAMP_40_US); //设置发射功率和RampTime
llcc68_set_buffer_base_address(context,0x0,0xff); //设置发送机发送与接收缓存指针
// llcc68_write_buffer(context,0x00,playload,34); //写有效载荷到Buffer里,这里注释掉,改在radiosend函数中进行
llcc68_set_lora_mod_params(context,&tx_lora_par); //设置Lora调制器参数
// llcc68_set_lora_pkt_params(context,&pkt_par); //设置Lorq数据包参数,这里注释掉,改在radiosend函数中进行
llcc68_set_dio_irq_params(context,0x01,0x01,0x00,0x00); //设置发送机中断相关参数,设置接收机中断相关参数,参见数据手册74页。
llcc68_write_register(context,0x0740,sync_word,2); //设置同步字符
// llcc68_set_tx(context,0); //设置发送机进入发送状态,发送完后进入待机状态,这里注释掉,改在radiosend函数中进行
}
void radiosend(const void* context,uint8_t *playload,uint8_t length) //发送函数
{
tx_pkt_par.pld_len_in_bytes = length;
llcc68_set_lora_pkt_params(context,&tx_pkt_par); //设置Lora PKT包参数,这里主要是修改了有效载荷长度
llcc68_write_buffer(context,0x00,playload,length); //写有效载荷到缓存
llcc68_set_tx(context,0); //切换至发送状态,发送完成后转为待机状态
}
void radio_0_irq(void)
{
radio_irq_flag|=0x01;
}
void radio_1_irq(void)
{
radio_irq_flag|=0x2;
}
void radio_irq_processing(const void *context)
{
uint16_t irq_number;
llcc68_get_irq_status(context,&irq_number);
llcc68_clear_irq_status(context,LLCC68_IRQ_ALL);
if (irq_number==1)
{
printf("Send success!\n");
}
if (irq_number==2)
{
radioreceive(context, rx_buff);
}
}
void radioreceive(const void* context, uint8_t *rxbuff) //接收处理函数
{
llcc68_get_lora_pkt_status(context, &last_pkt_status);
llcc68_get_rx_buffer_status(context, &last_rxbuff_status);
// printf("RX Length is %d \n", last_rxbuff_status.pld_len_in_bytes);
// printf("Buffer_start_pointer is %d \n", last_rxbuff_status.buffer_start_pointer);
llcc68_read_buffer(context, last_rxbuff_status.buffer_start_pointer, rxbuff, last_rxbuff_status.pld_len_in_bytes );
// printf("~~%d~~%d~~%d~~",last_pkt_status.rssi_pkt_in_dbm,last_pkt_status.signal_rssi_pkt_in_dbm,last_pkt_status.snr_pkt_in_db);
printf("%s",rxbuff);
// printf("~~%d~~%d~~%d~~",last_pkt_status.rssi_pkt_in_dbm,last_pkt_status.signal_rssi_pkt_in_dbm,last_pkt_status.snr_pkt_in_db);
clear_rxbuff();
}
void clear_rxbuff(void) //清空接收缓存数组
{
uint16_t i=0;
for (i=0;i<256;i++)
{
rx_buff[i]=0;
}
}
然后看看main.c里如何调用它们
#include "config.h"
#define PRINT //启用printf 重定向功能。
/*
* printf 重定向
初始化串口后需要执行 stdout = &mystdout;
*/
#ifdef PRINT
static int uart_putchar(char c, FILE *stream);
static FILE mystdout = FDEV_SETUP_STREAM(uart_putchar, NULL,_FDEV_SETUP_WRITE);
static int uart_putchar(char c, FILE *stream) //自定义的putchar
{
while(!(UCSRA&0x20));
UDR = c;
return 0;
}
#endif
void exit_init(void) //外部中断初始化
{
cli();
MCUCR = 0x0f;
GICR = 0xc0;
TIMSK = 0x00;
sei();
}
ISR (INT0_vect) //INT0中断服务函数
{
radio_0_irq();
};
ISR (INT1_vect) //INT0中断服务函数
{
radio_1_irq();
};
uint16_t i=0;
int main(void)
{
/* Replace with your application code */
usart_init(); //串口初始化
#ifdef PRINT
stdout = &mystdout;
#endif
spi_init(); //SPI口初始化
exit_init(); //外部中断初始化
llcc68_interface_init(); //连接在Lora模块上的引脚初始化
radiorx_init(&llcc68_which_1); //模块2初始化成接收模式
radiotx_init(&llcc68_which_0); //模块1初始化成发送模式
while (1)
{
if ((radio_irq_flag&0x01)==0x01) //当INT0中断时
{
radio_irq_processing(&llcc68_which_0); //模块1中断处理
radio_irq_flag &= 0xfe; //清除中断标志
}
if ((radio_irq_flag&0x02)==0x02) //当INT1中断时
{
radio_irq_processing(&llcc68_which_1); //模块2中断处理
radio_irq_flag &= 0xfd; //清除中断标志
}
_delay_us(100);
if(--usart_rx_timeout==0) //约10多个毫秒没有收到数据,认为串口接收一帧完整数据
{
usart_rx_timeout=TimeOutMax;
if(usart_read_index!=usart_write_index) //判断是否真的收到数据
{
usart_read_index=0; //串口读指针清零,暂时没用上
usart_write_index=0; //串口写指针清零
radiosend(&llcc68_which_0,usart_rx_buff,usart_rx_counter); //向Lora写入有效载荷并发送
usart_rx_counter=0; //清零串口接收计数器
clear_rxbuff(); //清空串口接收缓存
}
}
}
}
另外LLCC68 扩频因子与接收带宽应遵循以下内容,超出范围不能正常收发:
太多原理性的东西这里不探讨,驱动函数对应Datasheet上提到的指令集,可以参照Datasheet来理解。我这主控用的 ATMEGA16,原理图上是ATMEGA32,它们引脚完全兼容的,16的FLASH小点而已。程序编译工具为:Microchip Studio 7.0.2594,会在最后给出全套资料(原理图,代码,打样Gerber文件),供有需要同学参考。
看看我凌乱的调试现场:
测试收发了1000包数据(4.7万个字节),暂无丢包出现,但也不能代表就不会出现丢包。。。
就写到这里吧。
全套资料下载地址:https://download.csdn.net/download/dream52/88305645。