SI24R1是一款性能出色且应用广泛的国产2.4GHz无线收发芯片,SI24R1在软件和硬件上完全兼容 Nordic 公司的 nRF24L01+。这意味着,大多数为 nRF24L01+ 编写的代码和现有电路板,可以无缝迁移或直接使用 SI24R1,项目移植和开发门槛大大降低。
SI24R1支持多种发射功率等级。在需要更远通信距离时,可以设置为最高的+7dBm;在近距离或对功耗要求极严苛的场景,可以调低功率以节省能耗。
芯片集成了ARQ(自动重传请求)基带协议引擎,自动处理数据包的应答、重传和校验。这能减轻主控MCU的负担,简化编程,并提高通信的可靠性。
下面这个表格汇总了它的核心特性:
| 特性类别 | 具体参数 |
|---|---|
| 📡 工作频段 | 2400-2525MHz ISM 频段 |
| 📶 发射功率 | 可调,最高 +7dBm |
| 💨 数据传输速率 | 2Mbps, 1Mbps, 250Kbps |
| 🔋 功耗表现 | 关断模式 0.7μA ,待机模式 15μA |
| ⚡ 工作电压 | 1.9V - 3.6V |
| 🤝 接口与协议 | SPI 接口,内置 ARQ 基带协议引擎 |
| 🏭 封装形式 | QFN20 或 COB 封装 |
今天我们用之前开源的stm32四轴飞行器的代码来介绍一下stm32基于两个SI24R1(NRF24L01)的遥控控制原理以及遥控代码的移植教程,这样大家就可以通过两个32和两个SI24R1(NRF24L01)自行制作遥控赛车以及各种遥控小玩意了。这里的SI24R1是通过SPI通信的,SPI通信原理之前我也说过一点,感兴趣的可去之前的文章自行观看。其实我们想要使用遥控部分的代码,主要需要自己diy的部分就是下图中两个摇杆和几个按键以及自己diy想要显示的oled内容,其它的SI24R1通信原理部分都是复制粘贴即可,当然感兴趣的也可以深入了解一下。

1.1.首先看遥控器端的主函数main.c,其中必须移植的代码已经在注释里标出来了,接下来我们一一介绍必须移植的代码部分。
cs
extern u8 ADC_CALIBRATOR_OK;
int main(void)
{
USART_Config();//串口初始化,非必须移植,调试用
//printf("usart is ready\r\n");
SysTick_Init();//系统时钟初始化
OLED_Init();//显示屏初始化,建议移植
NVIC_Config();//中断优先级管理初始化,必须移植
BUTTON_Config();//按键初始化,必须移植
LED_Config();//led灯初始化,非必须移植,指示灯
LED_On(LED1);//点亮led1
SI24R1_Config();//NRF24L01初始化,必须移植
TIM_OCTigrConfig();//定时器初始化,必须移植
ADC_DmaConfig();//这个必须移植且最后一个配置,因为一旦配置好这个,ADC就会开始工作了,则DMA会开始每个一段时间产生中断,或者先关闭总中断,最后所有都配置完毕后在打开总中断
WaitFlY_Connection();//SI24R1等待连接/对频函数
Display_init();//oled初始化显示函数
while(1)
{
Display_Update();//OLED更新显示
ReconnectionFly();//断线重连
}
}
1.2.OLED的代码需要的可以私聊获取,就不单独讲了,首先看一下中断优先级管理,其中除了系统时钟使用到中断以外,两个SI24R1连接后IRQ引脚也会触发外部中断配置一下模式,提示一些信息(LED,可自定义)以及清除标志位等,还有DMA将ADC的数据运输完成会触发中断将摇杆的信息发送出去,以及按键触发的外部中断将按键的信息发送出去。
cs
//中断优先级组
#define NVIC_PRIORITY_GROUP_3 3 /* Preemption: 4 bits / Subpriority: 0 bits */
#define NVIC_PRIORITY_GROUP_4 4 /* Preemption: 3 bits / Subpriority: 1 bits */
#define NVIC_PRIORITY_GROUP_5 5 /* Preemption: 2 bits / Subpriority: 2 bits */
#define NVIC_PRIORITY_GROUP_6 6 /* Preemption: 1 bits / Subpriority: 3 bits */
#define NVIC_PRIORITY_GROUP_7 7 /* Preemption: 0 bits / Subpriority: 4 bits */
//为所有用到的中断配置抢占优先级和响应优先级
void NVIC_Config(void)
{
//SysTick_IRQn中断由于在有些中断函数中也会使用到,所以它的优先级是最高的
NVIC_SetPriority(SysTick_IRQn, NVIC_EncodePriority(NVIC_PRIORITY_GROUP_5, 0, 0)); //SysTick_IRQn中断
NVIC_SetPriority(EXTI1_IRQn, NVIC_EncodePriority(NVIC_PRIORITY_GROUP_5, 1, 0)); //EXTI1_IRQn中断,SI24R1的IRQ引脚中断
NVIC_SetPriority(DMA1_Channel1_IRQn, NVIC_EncodePriority(NVIC_PRIORITY_GROUP_5, 1, 1)); //DMA1_Channel1_IRQn中断,DMA将ADC数据传输完成中断
NVIC_SetPriority(EXTI15_10_IRQn, NVIC_EncodePriority(NVIC_PRIORITY_GROUP_5, 2, 2)); //EXTI15_10_IRQn中断,按键触发中断(优先级必须小于EXTI1_IRQn否则会导致SI24R1l连接中断)
}
1.3.因为按键的初始化代码就是对gpio的初始化,就不细看了我们只需知道是哪几个gpio口即可,后面会说按键触发外部中断的代码,接下来看SI24R1的代码,这部分主要是SI24R1的初始化代码以及一些模式配置,发送数据,连接对频的包装函数这些对于我们使用来说都是不用动的,直接移植过去即可,唯一可以diy的就是SI24R1的IRQ引脚触发的外部中断里面的led指示灯可根据自己喜好diy。这里的逻辑是:
-
LED1闪烁:连接成功但未解锁
-
LED1常亮:已解锁并通信中
-
LED1熄灭:通信中断
cs
u8 TX_ADDRESS[TX_ADR_WIDTH]= {0x34,0x43,0x10,0x10,0xFF}; //此地址用来识别接收端哪个RX通道可以接收发送出去的数据包
u8 RX_ADDRESS[RX_ADR_WIDTH]= {0x34,0x43,0x10,0x10,0xFF}; //此地址用来配置本机SI24R1的RX0通道的地址,同时为了能正常收到应答信号,此地址一般都和上面的地址配置相同
//发生中断时,根据STATUS寄存器中的值来判断是哪个中断源触发了IRQ中断
#define TX_DS 0x20 //数据发送完成中断
#define RX_DR 0x40 //数据接收完成中断
#define MAX_RT 0x10 //数据包重发次数超过设定值中断
#define AddrMax 50 //SI24R1最后一位地址最大值
//=====================================全局变量定义区========================================
vu8 sta; //接收从STATUS寄存器中返回的值
u8 RxBuf[RX_PLOAD_WIDTH]; //数据接收缓冲区,数据发送缓冲区
u8 FLYDataRx_OK = 0; //飞机数据接收完成
u8 FLY_Connect_OK = 0; //遥控与飞机已连接
u8 Reconnection_flag = 0; //遥控与飞机已断开需要重连
extern vu8 ButtonMask; //用来保存哪个按键刚被按下了,原始定义在:button.c文件中
extern u16 TX_CNT ; //遥控数据发送计数,原始定义在:senddata.c文件中
extern u16 TX_ERROR ; //遥控数据发送失败计数,原始定义在:senddata.c文件中
extern u8 ADC_CALIBRATOR_OK; //遥控通道ADC校准标志,原始文件在:adc_dma.h中
//========================================================================================
/**********************************************************************
初始化SI24R1
***********************************************************************/
void SI24R1_Config(void)
{
GPIO_InitTypeDef GPIO_InitStructus;
EXTI_InitTypeDef EXTI_initStructure;
//初始化SPI接口
SPI_Config();
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB|RCC_APB2Periph_AFIO,ENABLE);
//初始化CE引脚
GPIO_InitStructus.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructus.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructus.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB,&GPIO_InitStructus);
//配置IRQ引脚
GPIO_InitStructus.GPIO_Pin = GPIO_Pin_1;
GPIO_InitStructus.GPIO_Mode = GPIO_Mode_IPU; //nRF中断产生时,IRQ引脚会被拉低,所以这里要配置成上拉输入
GPIO_InitStructus.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB,&GPIO_InitStructus);
EXTI_initStructure.EXTI_Line = EXTI_Line1;
EXTI_initStructure.EXTI_LineCmd = ENABLE;
EXTI_initStructure.EXTI_Mode = EXTI_Mode_Interrupt;
EXTI_initStructure.EXTI_Trigger = EXTI_Trigger_Falling; //下降沿触发外部中断
EXTI_Init(&EXTI_initStructure);
GPIO_EXTILineConfig(GPIO_PortSourceGPIOB,GPIO_PinSource1);//开启GPIO管脚的中断线路
NVIC_EnableIRQ(EXTI1_IRQn);
CE_LOW; //拉低CE,注意:读/写nRF寄存器均需要将CE拉低,使其进入待机或者掉电模式才可以
//初始化SI24R1
SPI_Write_Byte(WRITE_REG_CMD + SETUP_AW, 0x03); //配置通信地址的长度,默认值时0x03,即地址长度为5字节
SPI_Write_Buf(WRITE_REG_CMD + TX_ADDR, TX_ADDRESS, TX_ADR_WIDTH);
SPI_Write_Buf(WRITE_REG_CMD + RX_ADDR_P0, RX_ADDRESS, RX_ADR_WIDTH);
SPI_Write_Byte(WRITE_REG_CMD + SETUP_RETR, 0x1a); //自动重发延迟为500+86us,重发次数10次
SPI_Write_Byte(WRITE_REG_CMD + EN_AA, 0x01); //接收数据后,只允许频道0自动应答
SPI_Write_Byte(WRITE_REG_CMD + EN_RXADDR, 0x01); //只允许频道0接收数据
SPI_Write_Byte(WRITE_REG_CMD + RF_SETUP, 0x27); //设置发射速率为2MHZ,发射功率为最大值0dB
SPI_Write_Byte(WRITE_REG_CMD + RF_CH, 30); //设置通道通信频率,工作通道频率可由以下公式计算得出:Fo=(2400+RF-CH)MHz.并且射频收发器工作的频率范围从2.400-2.525GHz
SPI_Write_Byte(WRITE_REG_CMD + RX_PW_P0, RX_PLOAD_WIDTH); //设置接收数据长度,本次设置为5字节,只有接收的数据达到此长度时,才会触发RX_DS中断
SPI_Write_Byte(WRITE_REG_CMD + CONFIG, 0x0f); //默认处于接收模式
CE_HIGH;
}
//IRQ引脚对应的EXTI外部中断处理函数
void EXTI1_IRQHandler(void)
{
CE_LOW; //拉低CE,以便读取SI24R1中STATUS中的数据
sta = SPI_Read_Byte(READ_REG_CMD+STATUS); //读取STATUS中的数据,以便判断是由什么中断源触发的IRQ中断
if(sta & TX_DS) //数据发送成功,并且收到了应答信号
{
RX_Mode(); //将SI24R1的模式改为接收模式,等待接收数据
if(ButtonMask & 0x01)
{
LED_On(LED1); //LED1常亮表示飞机已经解锁成功,并正在进行数据通讯,这里可自行diy
}else
{
LED_Toggle(LED1); //LED1闪烁表示飞机加锁模式中,但是和飞机通讯成功,这里可自行diy
}
SPI_Write_Byte(WRITE_REG_CMD+STATUS,TX_DS); //清除TX_DS中断
SPI_Write_Byte(FLUSH_TX,0xff); //清空TX_FIFO
}
else if(sta & RX_DR) //数据接收成功
{
FLY_Connect_OK = 1; //飞机与遥控器已连接
SI24R1_ReceivePacket(RxBuf); //将数据从RX端的FIFO中读取出来
FLYDataRx_OK = 1;
SPI_Write_Byte(WRITE_REG_CMD+STATUS,RX_DR); //清除RX_DR中断
}
else if(sta & MAX_RT) //触发了MAX_RT中断
{
if(TX_ERROR++>100) //遥控数据丢帧计数
{
Reconnection_flag = 1; //重连标志置位
FLY_Connect_OK = 0 ; //飞机与遥控器断开连接
}
LED_Off(LED1); //LED1灭时,表示和飞机通讯中断,这里可自行diy
RX_Mode(); //将SI24R1的模式改为接收模式,等待接收数据
SPI_Write_Byte(WRITE_REG_CMD+STATUS,MAX_RT); //清除MAX_RT中断
SPI_Write_Byte(FLUSH_TX,0xff); //清空TX_FIFO
}
EXTI_ClearITPendingBit(EXTI_Line1);
}
/**********************************************************************
配置SI24R1为RX模式,准备开始接收数据
***********************************************************************/
void RX_Mode(void)
{
CE_LOW; //拉低CE,进入待机模式,准备开始往SI24R1中的寄存器中写入数据
SPI_Write_Byte(WRITE_REG_CMD + CONFIG, 0x0f); //配置为接收模式
SPI_Write_Byte(WRITE_REG_CMD + STATUS, 0x7e); //写0111 xxxx 给STATUS,清除所有中断标志,防止一进入接收模式就触发中断
CE_HIGH; //拉高CE,准备接受从外部发送过来的数据
}
/**********************************************************************
从SI24R1的RX的FIFO中读取一组数据包
输入参数rx_buf:FIFO中读取到的数据的保存区域首地址
***********************************************************************/
void SI24R1_ReceivePacket(u8* rx_buf)
{
CE_LOW;
SPI_Read_Buf(RD_RX_PLOAD,rx_buf,RX_PLOAD_WIDTH); //从RX端的FIFO中读取数据,并存入指定的区域,注意:读取完FIFO中的数据后,SI24R1会自动清除其中的数据
SPI_Write_Byte(FLUSH_RX,0xff); //清除接收FIFO(很必要)
CE_HIGH; //重新拉高CE,让其重新处于接收模式,准备接收下一个数据
}
/**********************************************************************
配置SI24R1为TX模式,并发送一个数据包
输入参数tfbuf:即将要发送出去的数据区首地址
***********************************************************************/
void SI24R1_SendPacket(u8* tfbuf)
{
CE_LOW; //拉低CE,进入待机模式,准备开始往SI24R1中的寄存器中写入数据
// SPI_Write_Buf(WRITE_REG_CMD + RX_ADDR_P0, TX_ADDRESS, TX_ADR_WIDTH); //装载接收端地址,由于这里只有一个通道通讯,不用改变接收端的SI24R1的接收通道地址,所以,这句可以注释掉
SPI_Write_Buf(WR_TX_PLOAD, tfbuf, TX_PLOAD_WIDTH); //将数据写入TX端的FIFO中,写入的个数与TX_PLOAD_WIDTH设置值相同
SPI_Write_Byte(WRITE_REG_CMD + CONFIG, 0x0e); //将SI24R1配置成发射模式
SPI_Write_Byte(WRITE_REG_CMD + STATUS, 0x7e); //写0111 xxxx 给STATUS,清除所有中断标志,防止一进入发射模式就触发中断
CE_HIGH; //拉高CE,准备发射TX端FIFO中的数据
delay_ms(1); //CE拉高后,需要延迟至少130us
}
/* 遥控器飞机对频(其实是对地址) */
void WaitFlY_Connection(void)
{
static u8 cnt = 0,preaddr;
ConnectingDisplay();//断线连接状态显示
while(1)
{
if(FLY_Connect_OK)
{
cnt = 0;
if(preaddr != TX_ADDRESS[TX_ADR_WIDTH-1])
{
PID_WriteFlash();//保存上一次连接的飞机的SI24R1地址
// printf("Address save :%d preaddr:%d\r\n",TX_ADDRESS[4],preaddr);
}
// printf("Fly connect OK!!!\r\n");
return;
}else if(cnt++ < 10)
{
PID_ReadFlash();//读取上一次保存的飞机的SI24R1地址
preaddr = TX_ADDRESS[TX_ADR_WIDTH-1];
SI24R1_Config();
delay_ms(50);
// printf("Flash read SI24R1addr:%d\r\n",TX_ADDRESS[4]);
}
else
{
TX_ADDRESS[TX_ADR_WIDTH-1]++ ;
RX_ADDRESS[TX_ADR_WIDTH-1]++ ;
if(TX_ADDRESS[TX_ADR_WIDTH-1]>AddrMax && RX_ADDRESS[TX_ADR_WIDTH-1]>AddrMax)
{
TX_ADDRESS[TX_ADR_WIDTH-1] = 0x00;
RX_ADDRESS[TX_ADR_WIDTH-1] = 0x00;
}
SPI_Write_Buf(WRITE_REG_CMD + TX_ADDR, TX_ADDRESS, TX_ADR_WIDTH);
SPI_Write_Buf(WRITE_REG_CMD + RX_ADDR_P0, RX_ADDRESS, RX_ADR_WIDTH);
delay_ms(100);
}
if(ADC_CALIBRATOR_OK)
{
OLED_ShowString(byte(2),line4,"Calibration",8);
}
else
{
OLED_ShowString(byte(2),line4,"Connecting...",8);
}
}
}
//断线重连函数
void ReconnectionFly(void)
{
if(Reconnection_flag)
{
ConnectingDisplay();
if(FLY_Connect_OK) //遥控已重新连接
{
Display_init();
Reconnection_flag = 0; //断线重连标志复位
return;
}
SI24R1_Config();
}
}
1.4.然后看一下定时器初始化,这里主要利用定时器TIM4的通道4的输出比较功能,给ADC1提供采样触发信号,也就是定时器4中断触发一次,adc就采样一次摇杆信息,这里触发周期为:100ms,也是直接复制移植使用即可。
cs
void TIM_OCTigrConfig(void)
{
TIM_TimeBaseInitTypeDef TIM_timeBaseStucture;
TIM_OCInitTypeDef TIM_ocInitStructure;
u16 period = 10000; //设置周期值100ms发送一次
u16 pluse = 5000; //设置CRR值,这里的值可以随便设置,不影响ADC采样周期,但要保证在正确的范围内
//使能TIM4的时钟,TIM4是挂在APB1总线上的,注意TIM4的时钟是72M的
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4,ENABLE);
TIM_timeBaseStucture.TIM_ClockDivision = TIM_CKD_DIV1; //分频因子,输出给定时器的ETRP数字滤波器提供时钟
TIM_timeBaseStucture.TIM_Prescaler = 720-1; //预分频,给TIMx_CNT驱动的时钟120
TIM_timeBaseStucture.TIM_CounterMode = TIM_CounterMode_Up; //向上计数
TIM_timeBaseStucture.TIM_Period = period; //设置周期,给ARR赋值
TIM_TimeBaseInit(TIM4,&TIM_timeBaseStucture);
//配置TIM4通道4的输出比较
TIM_ocInitStructure.TIM_OCMode = TIM_OCMode_PWM1; //PWM输出模式为PWM1
TIM_ocInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; //设置有效电平的极性
TIM_ocInitStructure.TIM_OutputState = TIM_OutputState_Enable; //使能通道输出
TIM_ocInitStructure.TIM_Pulse = pluse; //设置PWM的脉冲宽度值,即CRR值
TIM_OC4Init(TIM4,&TIM_ocInitStructure);
TIM_ARRPreloadConfig(TIM4,ENABLE); //使能TIM4的寄存器ARR的预装载功能,DISABLE时将会使改变ARR值时立即生效
TIM_OC4PreloadConfig(TIM4,TIM_OCPreload_Enable); //使能TIM4通道1的CCR的预装载功能,DISABLE时将会使改变CRR值时立即生效
//使能TIM4定时器
TIM_Cmd(TIM4,ENABLE);
}
1.5.接下来看一下**ADC(采集摇杆信息用)**的代码,这部分包括了ADC的初始化(包含DMA)和ADC通道的校准函数(消除误差用),也是直接移植过去即可,看到这里你会发现初始化函数都说完了,主循环里也没有遥控的代码,需要我们修改移植的遥控代码到底在哪?别急,他们都在中断里,另外封装了函数,下面就说。
cs
u8 ADC_CALIBRATOR_OK; //遥控通道ADC校准标志
//用来保存ADC各通道转换完成后的数据
vu16 ADC_ConvertedValue[4]; //遥控通道ADC值
vu16 ADC_Calibrator[4]; //遥控通道ADC校准值
void ADC_DmaConfig(void)
{
GPIO_InitTypeDef GPIO_initStructure;
DMA_InitTypeDef DMA_initStructure;
ADC_InitTypeDef ADC_initStructure;
//开启DMA1时钟
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1,ENABLE);
//配置DMA1通道1,将ADC采样转换得到的数据传输到内存数组中
DMA_initStructure.DMA_BufferSize = 4; //每次传输的数据的个数,传输完时触发中断
DMA_initStructure.DMA_DIR = DMA_DIR_PeripheralSRC; //传输方向为:外设->内存
DMA_initStructure.DMA_M2M = DMA_M2M_Disable; //失能内存到内存的传输方式
DMA_initStructure.DMA_MemoryBaseAddr = (u32)ADC_ConvertedValue; //数据保存到内存中数组的首地址(这里因为ADC_ConvertedValue是数组名,所以不用加&)
DMA_initStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord; //以16位为单位进行数据的传输
DMA_initStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; //内存地址自增(这里地址是每次增加1,因为是以半字(16位)作为单位传输的)
DMA_initStructure.DMA_Mode = DMA_Mode_Circular; //循环传输的方式,这里必须为循环传输方式,否则会导致DMA只能传输一次
DMA_initStructure.DMA_PeripheralBaseAddr = ((u32)&ADC1->DR); //&ADC1->DR
DMA_initStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; //以半字为单位进行数据的传输
DMA_initStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; //外设地址固定
DMA_initStructure.DMA_Priority = DMA_Priority_Medium; //DMA通道1的优先级设置为中级,(这个优先级是当同一个DMA的不同通道同时有传输数据的要求时,优先级高的先进行传输)
DMA_Init(DMA1_Channel1,&DMA_initStructure);
DMA_ITConfig(DMA1_Channel1,DMA_IT_TC,ENABLE); //打开DMA通道1数据传输完成中断
NVIC_EnableIRQ(DMA1_Channel1_IRQn); //打开NVIC中对应的DMA通道1的中断通道
//开启DMA1的通道1
DMA_Cmd(DMA1_Channel1,ENABLE);
//使能ADC时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_ADC1,ENABLE);
GPIO_initStructure.GPIO_Pin = GPIO_Pin_0|GPIO_Pin_1|GPIO_Pin_2|GPIO_Pin_3; //ADC1的通道0,1,2,3
GPIO_initStructure.GPIO_Mode = GPIO_Mode_AIN; //ADC输入管脚需要为模拟输入模式
GPIO_Init(GPIOA,&GPIO_initStructure);
//配置ADC1
ADC_initStructure.ADC_ContinuousConvMode = DISABLE; //单次采样模式,每次由TIM4的CCR触发采样开始
ADC_initStructure.ADC_DataAlign = ADC_DataAlign_Right; //数据对齐模式为:右对齐
ADC_initStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_T4_CC4; //TIM4的通道4的CCR触发采样
ADC_initStructure.ADC_Mode = ADC_Mode_Independent; //各通道独立
ADC_initStructure.ADC_NbrOfChannel = 4; //一共要采样的通道的数目
ADC_initStructure.ADC_ScanConvMode = ENABLE; //打开扫描模式,由于这里有四个通道要采集,所以开始用扫描模式
ADC_Init(ADC1,&ADC_initStructure);
//开启ADC
ADC_Cmd(ADC1,ENABLE);
//开启ADC------DMA数据传输通道
ADC_DMACmd(ADC1,ENABLE);
//配置ADC采样参考时钟的预分频值
RCC_ADCCLKConfig(RCC_PCLK2_Div8);
//配置ADC的规则通道的采样顺序和采样时间
ADC_RegularChannelConfig(ADC1,ADC_Channel_0,1,ADC_SampleTime_239Cycles5);
ADC_RegularChannelConfig(ADC1,ADC_Channel_1,2,ADC_SampleTime_239Cycles5);
ADC_RegularChannelConfig(ADC1,ADC_Channel_2,3,ADC_SampleTime_239Cycles5);
ADC_RegularChannelConfig(ADC1,ADC_Channel_3,4,ADC_SampleTime_239Cycles5);
ADC_ResetCalibration(ADC1); //重置ADC采样校准器,防止出现较大的误差
while(ADC_GetCalibrationStatus(ADC1)); //等待校准成功
ADC_StartCalibration(ADC1); //开启ADC采样状态
while(ADC_GetCalibrationStatus(ADC1)); //等到开启成功
//使能外部触发ADC采样
ADC_ExternalTrigConvCmd(ADC1,ENABLE);
}
//ADC通道校准函数,60次采样平均,消除随机误差
void ADC_Calibration(void)
{
static u8 cnt=0;// 采样计数
static vs32 temp1=0,temp2=0,temp3=0,temp4=0;// 各通道累加和
if(ADC_ConvertedValue[2]>1900&&ADC_ConvertedValue[2]<2196&&ADC_ConvertedValue[3]>1900&&ADC_ConvertedValue[3]<2196)
{
if(0==cnt)
{
temp1 = 0;
temp2 = 0;
temp3 = 0;
temp4 = 0;
cnt = 0;
}
temp1 += (1000 + ADC_ConvertedValue[0]*1000/4096);
temp2 += (1000 + ADC_ConvertedValue[1]*500/4096);
temp3 += (1000 + ADC_ConvertedValue[2]*1000/4096);
temp4 += (1000 + ADC_ConvertedValue[3]*1000/4096);
cnt++;
if(cnt >= 60)
{
ADC_Calibrator[0] = temp1/cnt;
ADC_Calibrator[1] = temp2/cnt;
ADC_Calibrator[2] = temp3/cnt;
ADC_Calibrator[3] = temp4/cnt;
ADC_CALIBRATOR_OK = 0;
cnt = 0;
PID_WriteFlash();
}
}
}
1.6.接下来我们看按键中断和DMA中断,这里基本上也是不用动直接移植过去即可,因为按键触发的动作代码需要我们diy的都在被遥控端,遥控端只需将按下按键的信息发送过去即可,在这里我们也能看到四个按键使用的gpio口,然后数据丢包函数直接移植过去即可,最后DMA中断函数先判断需不需要adc校准,不需要就直接将摇杆adc数据发送出去,需要就先校准再发送。这里区分摇杆和按键是通过数据打包的前导码来区分的,区分方式如下:
-
0x01= ADC采样定时触发(摇杆数据) -
0x08= 按键触发(按钮状态变化)
cs
/*
0x01:连续的控制数据(摇杆位置)ADC采样触发发送
0x08:离散的事件数据(按钮状态)按键触发发送
*/
u16 TX_CNT = 0; //遥控数据发送计数
u16 TX_ERROR = 0; //遥控数据发送失败计数
float TX_ERROR_PERCENT = 0; //遥控数据丢包率
extern u8 FLY_Connect_OK; //飞机数据接收完成
extern u8 Reconnection_flag; //遥控与飞机已连接
extern u8 ADC_CALIBRATOR_OK; //遥控通道ADC校准标志,原始文件在:adc_dma.h中
extern u8 packetData[11]; //打包后待发送的数据包,原始定义在:nRF.c文件中
extern vu8 ButtonMask; //标记哪个按钮被点击了,原始定义在:button.c文件中
extern vu16 ADC_ConvertedValue[4]; //用来保存ADC各通道转换完成后的数据,原始定义在:adc_dma.c中
//按键外部中断处理函数
//按键可在这里自行diy选择
void EXTI15_10_IRQHandler(void)
{
if(EXTI_GetITStatus(EXTI_Line14) != RESET){
if((GPIOB->IDR & GPIO_Pin_14) == RESET){ //注意:这里不能用==SET来判断,因为SET = !RESET,也就是说SET的数字代表值是非零
delay_ms(10); //延迟20ms
if((GPIOB->IDR & GPIO_Pin_14) == RESET){ //再检测一下按键的电平状态,若此时还是处于低电平状态,说明按键不是由于抖动触发的
while((GPIOB->IDR & GPIO_Pin_14) == RESET); //等待按钮释放
if(ADC_ConvertedValue[1]>=100) //油门拉倒最低,触发KEY1才是有效的(油门大于100就直接返回退出)
return;
ButtonMask ^= 0x01; //按键1第一次被按下时为0x01,再次被按下时为0x00,以此重复
PackData(0x08); //打包数据,并且前导码为0x08
SI24R1_SendPacket(packetData);
}
}
EXTI_ClearITPendingBit(EXTI_Line14); //清除EXTI中断标志
}else if(EXTI_GetITStatus(EXTI_Line15) != RESET){
if((GPIOB->IDR & GPIO_Pin_15) == RESET){
delay_ms(10); //延迟20ms
if((GPIOB->IDR & GPIO_Pin_15) == RESET){ //再检测一下按键的电平状态,若此时还是处于低电平状态,说明按键不是由于抖动触发的
while((GPIOB->IDR & GPIO_Pin_15) == RESET);
ButtonMask ^= 0x02; //按键2被按下
PackData(0x08);
SI24R1_SendPacket(packetData);
}
}
EXTI_ClearITPendingBit(EXTI_Line15);
}else if(EXTI_GetITStatus(EXTI_Line13) != RESET){
if((GPIOB->IDR & GPIO_Pin_13) == RESET){
delay_ms(10); //延迟20ms
if((GPIOB->IDR & GPIO_Pin_13) == RESET){ //再检测一下按键的电平状态,若此时还是处于低电平状态,说明按键不是由于抖动触发的
while((GPIOB->IDR & GPIO_Pin_13) == RESET);
ButtonMask |= 0x04; //按键3被按下
PackData(0x08);
SI24R1_SendPacket(packetData);
}
ButtonMask &= ~0x04;
}
EXTI_ClearITPendingBit(EXTI_Line13);
}else if(EXTI_GetITStatus(EXTI_Line12) != RESET){
if((GPIOB->IDR & GPIO_Pin_12)== RESET){
delay_ms(10); //延迟20ms
if((GPIOB->IDR & GPIO_Pin_12)== RESET){ //再检测一下按键的电平状态,若此时还是处于低电平状态,说明按键不是由于抖动触发的
while((GPIOB->IDR & GPIO_Pin_12)== RESET);
ButtonMask ^= 0x08; //按键4被按下
PackData(0x08);
SI24R1_SendPacket(packetData);
}
// ButtonMask &= ~0x08;
}
EXTI_ClearITPendingBit(EXTI_Line12);
}
}
//求遥控数据丢包率
void Get_TxErrorPercent(void)
{
if(TX_CNT++ == 100) //满100清零
{
TX_CNT = 0;
TX_ERROR = 0;
}
TX_ERROR_PERCENT = (float)TX_ERROR/(float)TX_CNT;
if(TX_ERROR_PERCENT == 1)
{
Reconnection_flag = 1; //重连标志置位
FLY_Connect_OK = 0 ; //飞机与遥控器断开连接
}
// printf("TX_CNT:%d TX_ERROR:%d baifen:%0.2f\r\n",TX_CNT,TX_ERROR,TX_ERROR_PERCENT);
}
//摇杆的adc数据就是从这里发送出去的
//DMA1通道1中断通道处理函数,触发此中断时,说明已经将数据从ADC转移到内存数组中,可以触发数据发送了
void DMA1_Channel1_IRQHandler(void)
{
if(DMA_GetITStatus(DMA1_IT_TC1)==SET)
{
CAL();// 检查是否满足校准触发条件
if(ADC_CALIBRATOR_OK)
{
ADC_Calibration();//满足校准触发条件,执行校准
}
PackData(0x01); //打包数据,并且前导码为0x01
SI24R1_SendPacket(packetData);// 发送数据包
Get_TxErrorPercent(); //求遥控数据丢包率
// printf("BattV:%0.2f\r\n",ADC_ConvertedValue[4]*3.3*2/4096);
DMA_ClearITPendingBit(DMA1_IT_TC1); //清除DMA1通道1传输完成中断
}
}
1.7.最后看数据打包函数,也就是遥控端我们对遥控的diy就是在这里进行的,这里四个函数,第一个ADC摇杆消抖函数,注释很详细就不细说了,第三个是接受被遥控端的信息,第四个是adc校准标志位判断函数用来判断是否满足校准条件的函数,重点说一下第二个数据打包函数,摇杆的ADC信息就是在这里进行打包发送出去的,因为ADC采集时0-4096的数据所以我们要将它转换成我们想要的数值,例如归一化到0-1000的范围内这样就可以对应被控制端想要控制的数据(例如遥控车的四个电机的PWM,或是舵机的0-180°的PWM,)在这里你可以对摇杆的四个ADC值自行diy转换成你想在被控制端使用的控制量。
cs
FLY_TYPE FLY;
u8 SENSER_OFFSET_FLAG;
u8 packetData[12]; //打包后待发送的数据包,这个长度必须和接收端SI24R1的接收通道定义的有效数据长度相同,否则接收不到数据
u8 dataPID = 0; //数据包识别PID
vu16 accelerator = 0; //记录油门上一次的数值,用于下拉油门的时候,防止动力损失过大造成的失衡
extern u8 RxBuf[RX_PLOAD_WIDTH];
extern vu8 ButtonMask; //用来保存哪个按键刚被按下了,原始定义在:button.c文件中
extern vu16 ADC_ConvertedValue[4]; //用来保存ADC各通道转换完成后的数据,原始定义在:adc_dma.c中
extern u8 ADC_CALIBRATOR_OK; //遥控通道ADC校准标志,原始文件在:adc_dma.h中
extern vu16 ADC_Calibrator[4]; //遥控通道ADC校准值,原始定义在:adc_dma.c中
vu16 ADC_Value[4];
/**************************************************************************************
打包数据
注意按照下面的通讯协议进行打包数据:以字节为单位:
前导码-按键MASK--ADC1低8--ADC1高8--ADC2低8--ADC2高8--ADC3低8--ADC3高8--ADC4低8--ADC4高8--数据包标识--校验码0xa5
其中:前导码只有0x01和0x08才表示有效的数据包,0x01表示此数据包是由ADC采样完成触发的,
0x08表示此数据包是由遥控器上的按键触发的,
数据包标识用于接收端SI24R1识别是否是同一数据包的作用(这在飞机上主要用于当遥控信号中断时,自动开始降落。)
**************************************************************************************/
/*
摇杆数据死区处理和限幅函数:
当摇杆值在(L, R)范围内时,强制输出中心值1500,消除摇杆微小抖动导致的控制指令波动
*/
vu16 ADC_ValueLimit(vu16 value,vu16 L,vu16 R,vu16 min,vu16 max)
{
vu16 result = 0;
if(value>L&&value<R)
value= 1500;
if(value < L)
value += 1500-L;
if(value > R)
value -= 1500-L;
value = (value<=min)?min:value;//防止越界
value = (value>=max)?max:value;//防止越界
result = value;
return result;
}
//摇杆adc数据打包,摇杆数据就在这里进行diy
void PackData(u8 firstByte)
{
// 通道0:YAW(偏航)
ADC_Value[0] = 1500 + ((1000+ADC_ConvertedValue[0]*1000/4096) - ADC_Calibrator[0]);// 得到1100-1900范围,1500为中心
ADC_Value[0] = ADC_ValueLimit(ADC_Value[0],1470,1530,1100,1900);
// 通道1:油门
ADC_Value[1] = (ADC_ConvertedValue[1]*1000/4096); //将油门归一化0~1000;
// 通道2:ROLL(横滚)
ADC_Value[2] = 1500 + ((1000+(ADC_ConvertedValue[2])*1000/4096) - ADC_Calibrator[2] );// 得到1100-1900范围,1500为中心
ADC_Value[2] = ADC_ValueLimit(ADC_Value[2],1470,1530,1100,1900);
// 通道3:PITCH(俯仰)
ADC_Value[3] = 1500 + ((1000+(ADC_ConvertedValue[3])*1000/4096) - ADC_Calibrator[3] );// 得到1100-1900范围,1500为中心
ADC_Value[3] = ADC_ValueLimit(ADC_Value[3],1470,1530,1100,1900);
//数据包识别PID自增,并且超过200时自动归零
if(dataPID>=200){
dataPID = 0;
}else{
dataPID++;
}
//=========直接采用指针操作内存中的数值将16位转成8位,速度快,并且不会发生精度截取的现象,还要注意,STM32是小端地址============
packetData[0] = firstByte; //前导码
packetData[1] = ButtonMask; //按键
packetData[2] = *(((u8*)ADC_Value)+0); //YAW角
packetData[3] = *(((u8*)ADC_Value)+1);
packetData[4] = *(((u8*)ADC_Value)+2); //油门数据打包
packetData[5] = *(((u8*)ADC_Value)+3);
packetData[6] = *(((u8*)ADC_Value)+4); //ROLL角
packetData[7] = *(((u8*)ADC_Value)+5);
packetData[8] = *(((u8*)ADC_Value)+6); //PITCH角
packetData[9] = *(((u8*)ADC_Value)+7);
packetData[10] = dataPID; //这个非常重要,这是防止飞机逃脱遥控的保证
packetData[11] = 0xa5; //校验码:1010 0101
}
//接收飞机发送过来的数值
void ReceiveDataAnalysis(void)
{
if(RxBuf[0]==0xFF )
{
SENSER_OFFSET_FLAG = RxBuf[1];
FLY.Thr = ((RxBuf[2]<<8)|RxBuf[3]);
FLY.Yaw = ((s16)(RxBuf[4]<<8)|RxBuf[5])/100;
FLY.Pit = ((s16)(RxBuf[6]<<8)|RxBuf[7])/100;
FLY.Rol = ((s16)(RxBuf[8]<<8)|RxBuf[9])/100;
FLY.Alt = ((s16)(RxBuf[10]<<8)|RxBuf[11])/100;
FLY.BattV = ((s16)(RxBuf[12]<<8)|RxBuf[13]);
// printf("SENSER_OFFSET_FLAG:0x%x\r\n",SENSER_OFFSET_FLAG);
// printf("Thr:%d Yaw:%d Rol:%d Pit:%d\r\n",Thr,Yaw,Rol,Pit);
}
}
void CAL(void)//遥控器校准触发标志位函数
{
if(ADC_ConvertedValue[2]<6 && ADC_ConvertedValue[3]<6 && ADC_ConvertedValue[1]<6)
{
ADC_CALIBRATOR_OK = 1;
// printf("遥控器校准!\r\n");
}
}
可以发现其实遥控器的代码需要自己diy的地方不多,基本上就只需要将四个摇杆adc的值转换成自己想控制的量的范围即可,然后就是oled显示的部分可能需要改成你自己想显示的东西,这方面留给大家自行diy了就不细说了。最后总结一下遥控器的控制逻辑:
🎮 控制逻辑详解
遥控器数据流:
-
定时触发:TIM4触发摇杆ADC采样 → DMA传输完成中断 → 打包发送
-
按键触发:按键中断 → 立即打包发送
-
状态反馈:接收小车回传数据 → 解析传感器信息
2.1.被遥控端代码我们就不从main.c一一介绍了,我们直接看使用到的部分,先看一下被遥控端的SI24R1代码,这部分代码和遥控端一样,是初始化代码和各种封装好的功能代码,直接复制移植即可。
cs
#define SI24R1AddrMax 50 //NRF最后一个字节地址最大为50
uint8_t SI24R1addr = 0xFF; //初始化NRF最后一字节地址
uint8_t SI24R1_TX_DATA[TX_PAYLO_WIDTH];//NRF发送缓冲区
uint8_t SI24R1_RX_DATA[RX_PAYLO_WIDTH];//NRF接收缓冲区
uint8_t TX_ADDRESS[TX_ADR_WIDTH]={0x34,0x43,0x10,0x10,0x01}; //发送地址
uint8_t RX_ADDRESS[RX_ADR_WIDTH]={0x34,0x43,0x10,0x10,0x01}; //接收地址
/*****************************************************************************
* 函 数:void SI24R1_Init(void)
* 功 能:NRF引脚GPIO初始化
* 参 数:无
* 返回值:无
* 备 注:无
*****************************************************************************/
void SI24R1_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB|RCC_APB2Periph_GPIOA,ENABLE);
/* 配置CSN引脚 */
GPIO_InitStruct.GPIO_Pin=GPIO_Pin_12;
GPIO_InitStruct.GPIO_Mode=GPIO_Mode_Out_PP;
GPIO_InitStruct.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Init(GPIOB,&GPIO_InitStruct);
GPIO_ResetBits(GPIOB,GPIO_Pin_12);
/* 配置CE引脚 */
GPIO_InitStruct.GPIO_Pin=GPIO_Pin_8;
GPIO_InitStruct.GPIO_Mode=GPIO_Mode_Out_PP;
GPIO_InitStruct.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStruct);
GPIO_ResetBits(GPIOA,GPIO_Pin_8);
SPI_GPIO_Init(); //SPI2初始化
SI24R1_Check(); //检查SI24R1是否与MCU通信
SI24R1_CSN_HIGH; //失能NRF
SI24R1_CE_LOW; //待机模式
}
/*****************************************************************************
* 函 数:uint8_t SI24R1_write_reg(uint8_t reg,uint8_t value)
* 功 能:写一字节数据到寄存器
* 参 数:reg: 寄存器地址
* val: 要写入的数据
* 返回值:status
* 备 注:SI24R1代码移植只需把SPI驱动修改成自己的即可
*****************************************************************************/
uint8_t SI24R1_write_reg(uint8_t reg,uint8_t value)
{
uint8_t status;
SI24R1_CSN_LOW;
status=SPI2_WriteReadByte(reg);
SPI2_WriteReadByte(value);
SI24R1_CSN_HIGH;
return status;
}
/*****************************************************************************
* 函 数:uint8_t SI24R1_read_reg(uint8_t reg)
* 功 能:读一字节数据到寄存器
* 参 数:reg: 寄存器地址
* 返回值:reg_val
* 备 注:SI24R1代码移植只需把SPI驱动修改成自己的即可
*****************************************************************************/
uint8_t SI24R1_read_reg(uint8_t reg)
{
uint8_t reg_val;
SI24R1_CSN_LOW;
SPI2_WriteReadByte(reg);
reg_val = SPI2_WriteReadByte(0xff);
SI24R1_CSN_HIGH;
return reg_val;
}
/*****************************************************************************
* 函 数:uint8_t SI24R1_Write_Buf(uint8_t reg, uint8_t *pBuf, uint8_t len)
* 功 能:写一组数据到寄存器
* 参 数:reg: 寄存器地址
* pBuf: 要写入数据的地址
* len: 要写入的数据长度
* 返回值:status
* 备 注:SI24R1代码移植只需把SPI驱动修改成自己的即可
*****************************************************************************/
uint8_t SI24R1_Write_Buf(uint8_t reg, uint8_t *pBuf, uint8_t len)
{
uint8_t status;
int i;
SI24R1_CSN_LOW;
status = SPI2_WriteReadByte(reg);
for( i=0;i<len;i++)
{
SPI2_WriteReadByte(*pBuf);
pBuf++;
}
SI24R1_CSN_HIGH;
return status;
}
/*****************************************************************************
* 函 数:uint8_t SI24R1_Read_Buf(uint8_t reg, uint8_t *pBuf, uint8_t len)
* 功 能:读一组数据到寄存器
* 参 数:reg: 寄存器地址
* pBuf: 要读取数据的地址
* len: 要读取的数据长度
* 返回值:status
* 备 注:SI24R1代码移植只需把SPI驱动修改成自己的即可
*****************************************************************************/
uint8_t SI24R1_Read_Buf(uint8_t reg, uint8_t *pBuf, uint8_t len)
{
uint8_t status;
int i;
SI24R1_CSN_LOW;
status = SPI2_WriteReadByte(reg);
for(i = 0;i < len ;i++)
{
*pBuf = SPI2_WriteReadByte(0xff);
pBuf++;
}
SI24R1_CSN_HIGH;
return status;
}
/*****************************************************************************
* 函 数:void SI24R1set_Mode(uint8_t mode)
* 功 能:切换SI24R1的工作模式模式
* 参 数:无
* 返回值:无
* 备 注:无
*****************************************************************************/
void SI24R1set_Mode(uint8_t mode)
{
if(mode == IT_TX)
{
SI24R1_CE_LOW;
SI24R1_write_reg(W_REGISTER+CONFIG,IT_TX);
SI24R1_write_reg(W_REGISTER+STATUS,0X7E); //清除所有中断,防止一进去发送模式就触发中断
SI24R1_CE_HIGH;
}
else
{
SI24R1_CE_LOW;
SI24R1_write_reg(W_REGISTER+CONFIG,IT_RX);//配置为接收模式
SI24R1_write_reg(W_REGISTER+STATUS,0X7E); //清除所有中断,防止一进去接收模式就触发中断
SI24R1_CE_HIGH;
Delay_us(200);
}
}
/*****************************************************************************
* 函 数:void SI24R1_Config(void)
* 功 能:SI24R1基本参数配置,并初始化为接收模式
* 参 数:无
* 返回值:无
* 备 注:无
*****************************************************************************/
void SI24R1_Config(void)
{
SI24R1_CE_LOW;
SI24R1_write_reg(W_REGISTER+SETUP_AW, 0x03); //配置通信地址的长度,默认值时0x03,即地址长度为5字节
SI24R1_Write_Buf(W_REGISTER+TX_ADDR,(uint8_t*)TX_ADDRESS,TX_ADR_WIDTH); //写TX节点地址
SI24R1_Write_Buf(W_REGISTER+RX_ADDR_P0,(uint8_t*)TX_ADDRESS,RX_ADR_WIDTH); //设置TX节点地址,主要为了使能ACK
SI24R1_write_reg(W_REGISTER+SETUP_RETR,0x1A); //设置自动重发间隔时间:500us + 86us;最大自动重发次数:10次 0x1A
SI24R1_write_reg(W_REGISTER+EN_RXADDR,0x01);//使能通道0的接收地址
SI24R1_write_reg(W_REGISTER+EN_AA,0x01); //使能通道0自动应答
SI24R1_write_reg(W_REGISTER+RX_PW_P0,RX_PAYLO_WIDTH);//选择通道0的有效数据宽度
SI24R1_Write_Buf(W_REGISTER+RX_ADDR_P0,(uint8_t*)RX_ADDRESS,RX_ADR_WIDTH); //写RX节点地址
SI24R1_write_reg(W_REGISTER+RF_CH,30); //设置RF通道为40hz(1-64Hz都可以)
SI24R1_write_reg(W_REGISTER+RF_SETUP,0x27); //设置TX发射参数,0db增益,2Mbps,低噪声增益关闭 (注意:低噪声增益关闭/开启直接影响通信,要开启都开启,要关闭都关闭0x0f)
SI24R1set_Mode(IT_RX); //默认为接收模式
SI24R1_CE_HIGH;
}
/*****************************************************************************
* 函 数:uint8_t SI24R1_TxPacket(uint8_t *txbuf)
* 功 能:SI24R1发送一包数据
* 参 数:txbuf:要发送数据地址
* 返回值:无
* 备 注:无
*****************************************************************************/
void SI24R1_TxPacket(uint8_t *txbuf)
{
SI24R1_CE_LOW;
SI24R1_Write_Buf(W_REGISTER+TX_ADDR,(uint8_t*)TX_ADDRESS,TX_ADR_WIDTH); //写TX节点地址
SI24R1_Write_Buf(W_REGISTER+RX_ADDR_P0,(uint8_t*)TX_ADDRESS,RX_ADR_WIDTH); //设置TX节点地址,主要为了使能ACK
SI24R1_Write_Buf(W_RX_PAYLOAD,txbuf,TX_PAYLO_WIDTH); //写数据到TX_BUFF
SI24R1_write_reg(W_REGISTER+CONFIG,0x0e); //设置为发送模式,开启所有中断
SI24R1_write_reg(W_REGISTER+STATUS,0X7E); //清除所有中断,防止一进去发送模式就触发中断
SI24R1_CE_HIGH;
Delay_us(10); //CE持续高电平10us
}
/*****************************************************************************
* 函 数:uint8_t SI24R1_RxPacket(uint8_t *rxbuf)
* 功 能:SI24R1接收一包数据
* 参 数:rxbuf:接收数据存储地址
* 返回值:无
* 备 注:无
*****************************************************************************/
void SI24R1_RxPacket(uint8_t *rxbuf)
{
SI24R1_CE_LOW;
SI24R1_Read_Buf(R_RX_PAYLOAD,rxbuf,TX_PAYLO_WIDTH);//读取RX的有效数据
SI24R1_write_reg(FLUSH_RX,0xff); //清除RX FIFO(注意:这句话很必要)
SI24R1_CE_HIGH;
}
/*****************************************************************************
* 函 数:uint8_t SI24R1_testConnection(void)
* 功 能:检查SI24R1与MCU的SPI总线是否通信正常
* 参 数:无
* 返回值:1已连接 0未连接
* 备 注:无
*****************************************************************************/
uint8_t SI24R1_testConnection(void)
{
uint8_t buf[5]={0XA5,0XA5,0XA5,0XA5,0XA5};
uint8_t i;
SI24R1_Write_Buf(W_REGISTER+TX_ADDR,buf,5); //写入5个字节的地址.
SI24R1_Read_Buf(TX_ADDR,buf,5); //读出写入的地址
for(i=0;i<5;i++)
if(buf[i]!=0XA5)break;
if(i!=5)return 0; //检测24L01错误
return 1; //检测到24L01
}
/*****************************************************************************
* 函 数:void SI24R1_Check(void)
* 功 能:检测SI24R1是否连接
* 参 数:无
* 返回值:无
* 备 注:无
*****************************************************************************/
void SI24R1_Check(void)
{
while(!SI24R1_testConnection())
{
printf("\r SI24R1 no connect...\r\n");
RGB_LED_Red();//红灯常亮
}
}
/*****************************************************************************
* 函 数:void SI24R1_GetAddr(void)
* 功 能:给飞机上的SI24R1获取一个地址
* 参 数:无
* 返回值:无
* 备 注:此函数需要与遥控器的对频函数联合使用否则SI24R1通信不成功,
如果自己做的的遥控器可直接用固定地址
*****************************************************************************/
void SI24R1_GetAddr(void)
{
if(SI24R1addr > SI24R1AddrMax)//当 SI24R1addr大于10,就说明此时SI24R1还未初始化完成
{
srand(SysTick->VAL);//给随机数种子
// printf("SysTick->VAL:%d\r\n",SysTick->VAL);
SI24R1addr = rand()%SI24R1AddrMax;//随机获取SI24R1最后一位地址(地址:0~50)
PID_WriteFlash();//保存此地址Flash
}else if(SI24R1addr != TX_ADDRESS[TX_ADR_WIDTH-1])
{
TX_ADDRESS[TX_ADR_WIDTH-1] = SI24R1addr;
RX_ADDRESS[TX_ADR_WIDTH-1] = SI24R1addr;
SI24R1_Config();
// printf("SI24R1Addr:%d\r\n",SI24R1addr);
}
}
/*****************************************************************************
* 函 数:void SI24R1_Test(void)
* 功 能:SI24R1通信测试函数
* 参 数:无
* 返回值:无
* 备 注:测试时用
*****************************************************************************/
void SI24R1_Test(void)
{
uint8_t t=0;
static uint8_t mode,key;
mode = ' ';
key=mode;
for(t=0;t<32;t++)
{
key++;
if(key>('~'))key=' ';
SI24R1_TX_DATA[t]=key;
}
mode++;
if(mode>'~')mode=' ';
SI24R1_TxPacket(SI24R1_TX_DATA);
}
2.2.然后看一下SI24R1的IRQ引脚中断代码,遥控器发送过来的信息就是在这里被接收的。
cs
/*****************************************************************************
* 函 数:void EXTI2_IRQHandler(void)
* 功 能:SI24R1(全双工)的外中断处理函数
* 参 数:无
* 返回值:无
* 备 注: SI24R1的所有中断事件都在此函数中给予相应的处理
*****************************************************************************/
void EXTI2_IRQHandler(void)
{
uint8_t sta;
if(EXTI_GetITStatus(EXTI_Line2) != RESET )
{
SI24R1_CE_LOW;//拉低CE,以便读取NRF中STATUS中的数据
sta = SI24R1_read_reg(R_REGISTER+STATUS); //读取STATUS中的数据,以便判断是由什么中断源触发的IRQ中断
/* 发送完成中断 TX_OK */
if(sta & TX_OK)
{
SI24R1set_Mode(IT_RX);
SI24R1_write_reg(W_REGISTER+STATUS,TX_OK); //清除发送完成标志·
SI24R1_write_reg(FLUSH_TX,0xff); //清除TX_FIFO
// printf("Sent OK!!!!\r\n");
}
/* 接收完成中断 RX_OK */
if(sta & RX_OK)
{
//接收遥控器的信息
SI24R1_RxPacket(SI24R1_RX_DATA);
Remote_Data_ReceiveAnalysis();
SI24R1_write_reg(W_REGISTER+STATUS,RX_OK); //清除发送完成标志·
// printf("Receive OK!!!!\r\n");
}
/* 达到最大重发次数中断 MAX_TX */
if(sta & MAX_TX)
{
SI24R1set_Mode(IT_RX);
SI24R1_write_reg(W_REGISTER+STATUS,MAX_TX);//清除接达到最大重发标志
SI24R1_write_reg(FLUSH_TX,0xff); //清除TX_FIFO
// printf("Sent Max Data!!!\r\n");
}
EXTI_ClearITPendingBit(EXTI_Line2);
}
}
2.3.接下来看一下remotedata.c的代码 ,摇杆和按键的信息就是在这里被接受并解析的,但是这里摇杆的信息只被接受并未使用,而按键的信息是在这里接受并被使用的,所以这里可以对按键控制的行为自行进行diy,就在代码这个函数里void Button_command(uint8_t Button)进行diy自己想要按键按下实现的功能即可。(例如上锁/解锁等操作)
cs
uint8_t DataID; //数据包ID
RC_TYPE RC_Control;//遥控器输入
void Button_command(uint8_t Button);
/***********************************************************************************************************************
* 函 数:void Remote_Data_ReceiveAnalysis(void)
* 功 能:遥控器数据包解析
* 参 数:无
* 返回值:无
* 备 注:通信协议:
前导码-按键MASK--ADC1低8--ADC1高8--ADC2低8--ADC2高8--ADC3低8--ADC3高8--ADC4低8--ADC4高8--数据包标识--校验码0xa5;
前导码只有0x01和0x08才表示有效的数据包,0x01表示此数据包是由ADC采样完成触发的,0x08表示此数据包是由遥控器上的按
键触发的;
数据包标识用于识别是否是同一数据包的作用(这在飞机上主要用于当遥控信号中断时,自动开始降落。)
************************************************************************************************************************/
void Remote_Data_ReceiveAnalysis(void)
{
SI24R1_Controlflag = 1;
if(SI24R1_RX_DATA[11]!=0xa5) //验证校验码是否为0xa5
return;
if(SI24R1_RX_DATA[0] & 0x01) //当数据包是由遥控器的ADC采样完成时触发发送时
{
RC_Control.YAW = SI24R1_RX_DATA[3]<<8|SI24R1_RX_DATA[2]; //ADC0
RC_Control.THROTTLE = SI24R1_RX_DATA[5]<<8|SI24R1_RX_DATA[4]; //ADC1
RC_Control.ROLL = SI24R1_RX_DATA[7]<<8|SI24R1_RX_DATA[6]; //ADC2
RC_Control.PITCH = SI24R1_RX_DATA[9]<<8|SI24R1_RX_DATA[8]; //ADC3
}
else if(SI24R1_RX_DATA[0] & 0x08) //当数据包是由遥控器的按键触发发送时
{
Button_command(SI24R1_RX_DATA[1]); //ButtonMask按键命令解析
}
DataID = SI24R1_RX_DATA[10];//将数据包识别PID值取出,覆盖之前的值,以表示信号链接正常
}
/**************************************************************************************************************
* 函 数:void Button_command(uint8_t Button)
* 功 能:按键命令解析
* 参 数:Button 按键命令数值
* 返回值:无
* 备 注:遥控器共有四个按键 K1 ,K2 ,K3 ,K4
* K1 :解锁上锁按键
* K2 : 用户自定义功能按键
* K3 : 传感器校准按键
* K4 : 模式选择按键
* 其中K1,K2,K3,K4 对应ButtonMask的低4位,K1对应0位 K2对应1位 K3对应2位 K4对应3位
* 0x00的时候表示4个按键都没有被触发,0000 1111 表示KEY1~KEY4按键都被按下
* 按键每按一次对应位取反一次(详细操作请参考遥控器源码 senddata.c )
***************************************************************************************************************/
void Button_command(uint8_t Button)
{
static uint8_t PreButton = 0;//前一个按键值
//遥控器K1按键
if((PreButton&0x01) != (Button&0x01))
{
if(Button&0x01)//飞机解锁
{
Airplane_Enable = 1;
RGB_LED_FLY();
RGB_LED_Off();
}else //飞机上锁
{
Airplane_Enable = 0;
RGB_LED_Off();
}
}
//遥控器K2按键
if((PreButton&0x02) != (Button&0x02))
{
/*
用户自行定义,可用来调试
*/
}
//遥控器K3按键
if(Button&0x04) //陀螺仪加速度计校准
{
SENSER_FLAG_SET(GYRO_OFFSET);
}
//遥控器K4按键
if((PreButton&0x08) != (Button&0x08))
{
if(Button&0x08)
{
SENSER_FLAG_SET(FLY_MODE); //无头模式
}else
{
SENSER_FLAG_RESET(FLY_MODE); //有头模式
}
}
PreButton = Button;
}
/**************************************************************************************************************
* 函 数:void UnControl_Land(void)
* 功 能:信号中断紧急降落
* 参 数:无
* 返回值:无
* 备 注:粗略处理,有待完善
***************************************************************************************************************/
void UnControl_Land(void)
{
RC_Control.THROTTLE -= 6;
if(RC_Control.THROTTLE <= 150)
RC_Control.THROTTLE = 150;
}
/**************************************************************************************************************
* 函 数:void SI24R1_SingalCheck(void)
* 功 能:信号中断检测
* 参 数:无
* 返回值:无
* 备 注:如果飞机处于解锁状态但是,当前数据包的ID等于前一个数据包的ID,这就说明遥控器与飞机断开连接
***************************************************************************************************************/
void SI24R1_SingalCheck(void)
{
static uint8_t PreDataID = 250;
if(SI24R1_Controlflag)
{
if(Airplane_Enable && DataID == PreDataID)//飞机与遥控断开连接
{
UnControl_Land(); //紧急降落处理
RGB_LED_Red(); //红灯常亮报警
}else if(Airplane_Enable && !BATT_LEDflag)//飞机遥控连接正常
{
RGB_LED_FLY(); //飞行指示灯
}
PreDataID = DataID;
}
}
/**************************************************************************************************************
* 函 数:void SendToRemote(void)
* 功 能:飞机状态数据发送给遥控器
* 参 数:无
* 返回值:无
* 备 注:注意:SI24R1单次发送最大32个字节,请勿越界
***************************************************************************************************************/
void SendToRemote(void)
{
int16_t temp;
if(Airplane_Enable)
{
SENSER_FLAG_SET(FLY_ENABLE); //解锁模式置位
}
else
{
SENSER_FLAG_RESET(FLY_ENABLE); //上锁模式复位
}
SI24R1_TX_DATA[0] = 0xFF;//帧头
SI24R1_TX_DATA[1] = SENSER_OFFSET_FLAG; //标志位组
temp = (u16)RC_Control.THROTTLE; //油门
SI24R1_TX_DATA[2] = Byte1(temp);
SI24R1_TX_DATA[3] = Byte0(temp);
temp = (int)(Att_Angle.yaw*100); //航向
SI24R1_TX_DATA[4] = Byte1(temp);
SI24R1_TX_DATA[5] = Byte0(temp);
temp = (int)(Att_Angle.pit*100); //俯仰
SI24R1_TX_DATA[6] = Byte1(temp);
SI24R1_TX_DATA[7] = Byte0(temp);
temp = (int)(Att_Angle.rol*100); //横滚
SI24R1_TX_DATA[8] = Byte1(temp);
SI24R1_TX_DATA[9] = Byte0(temp);
temp = (int)(FBM.AltitudeFilter*100); //高度留待
SI24R1_TX_DATA[10] = Byte1(temp);
SI24R1_TX_DATA[11] = Byte0(temp);
temp = (int)(BAT.BattMeasureV*100); //飞机电池电压
SI24R1_TX_DATA[12] = Byte1(temp);
SI24R1_TX_DATA[13] = Byte0(temp);
SI24R1_TxPacket(SI24R1_TX_DATA); //SI24R1发送函数
}
2.4.接下来看一下摇杆信息控制部分的代码 ,因为我这里是因无人机的控制部分代码为例,使用了串级PID,所以看起来比较复杂,但其实代码里的这四个rc_in->THROTTLE,rc_in->ROLL,rc_in->PITCH,rc_in->YAW参数就是两个摇杆上下左右四个参数,如果你是控制四轮四电机小车的话,只需用这四个值来diy控制四个电机的pwm即可。
cs
//角度环PID
PID_TYPE PID_ROL_Angle;
PID_TYPE PID_PIT_Angle;
PID_TYPE PID_YAW_Angle;
//角速度环PID
PID_TYPE PID_ROL_Rate;
PID_TYPE PID_PIT_Rate;
PID_TYPE PID_YAW_Rate;
//高度环PID
PID_TYPE PID_ALT_Rate;
PID_TYPE PID_ALT;
float Pre_THROTTLE,THROTTLE;
float Moto_PWM_1=0.0f,Moto_PWM_2=0.0f,Moto_PWM_3=0.0f,Moto_PWM_4=0.0f;
uint8_t SI24R1_Controlflag = 1,Airplane_Enable;
/******************************************************************************************
*函 数:void Control(FLOAT_ANGLE *att_in,FLOAT_XYZ *gyr_in, RC_TYPE *rc_in, uint8_t armed)
*功 能:姿态控制,角度环控制和角速度环控制
*参 数:att_in:测量值
* gry_in: MPU6050读取的角速度值
* rc_in : 遥控器设定值
* armed: 记录命令
*返回值:无
*备 注:RoboFly 小四轴机头与电机示意图
机头(Y+)
M1 ↑ M2
\ | /
\ | /
\ | /
------------------------+------------------------>X+
/ | \
/ | \
/ | \
M4 | M3
1. M1 M3电机逆时针旋转,M2 M4电机顺时针旋转
2. X:是MPU6050的 X 轴,Y:是MPU6050的 Y 轴,Z轴正方向垂直 X-Y 面,竖直向上
3. 绕 X 轴旋转为 PITCH 角
绕 Y 轴旋转为 ROLL 角
绕 Z 轴旋转为 YAW 角
4. 自己DIY时进行动力分配可以一个轴一个轴的分配,切勿三个轴同时分配。
*******************************************************************************************/
void Control(FLOAT_ANGLE *att_in,FLOAT_XYZ *gyr_in, RC_TYPE *rc_in, uint8_t armed)
{
FLOAT_ANGLE Measure_Angle,Target_Angle;
Measure_Angle.rol = att_in->rol; //陀螺仪测量的实时横滚角
Measure_Angle.pit = att_in->pit; //陀螺仪测量的实时俯仰角
Measure_Angle.yaw = att_in->yaw; //陀螺仪测量的实时偏航角
// 将遥控器PWM信号(1000-2000)转换为角度指令(-41.67° ~ +41.67°)
Target_Angle.rol = (float)((rc_in->ROLL-1500)/12.0f); //遥控器输入的目标横滚
Target_Angle.pit = (float)((rc_in->PITCH-1500)/12.0f);//遥控器输入的目标俯仰
Target_Angle.yaw = (float)((1500-rc_in->YAW)/12.0f); //遥控器输入的目标偏航
if(!SI24R1_Controlflag)
{
Target_Angle.yaw = 0; // 无线控制失效时锁定偏航
}else if(fabs(Target_Angle.yaw )<4)
{
Target_Angle.yaw = 0;// 小指令死区,避免微小漂移
}
if(1 == (GET_FLAG(FLY_MODE)))
{
Yaw_Carefree(&Target_Angle,&Measure_Angle);//无头模式
}
//角度环(外环)
PID_Postion_Cal(&PID_ROL_Angle,Target_Angle.rol,Measure_Angle.rol);//ROLL角度环PID (输入角度 输出角速度)
PID_Postion_Cal(&PID_PIT_Angle,Target_Angle.pit,Measure_Angle.pit);//PITH角度环PID (输入角度 输出角速度)
PID_Postion_Cal(&PID_YAW_Angle,Target_Angle.yaw,Measure_Angle.yaw);//YAW角度环PID (输入角度 输出角速度)
//角速度环(内环)
PID_Postion_Cal(&PID_ROL_Rate,PID_ROL_Angle.OutPut,(gyr_in->Y*RadtoDeg)); //ROLL角速度环PID (输入角度环的输出,输出电机控制量)
PID_Postion_Cal(&PID_PIT_Rate,PID_PIT_Angle.OutPut,-(gyr_in->X*RadtoDeg)); //PITH角速度环PID (输入角度环的输出,输出电机控制量)
PID_Postion_Cal(&PID_YAW_Rate,-PID_YAW_Angle.OutPut,-(gyr_in->Z*RadtoDeg)); //YAW角速度环PID (输入角度,输出电机控制量)
//动力分配(自己DIY时动力分配一定要好好研究,动力分配搞错飞机肯定飞不起来!!!)
/*混控原理:
油门:所有电机基础推力
横滚:1、4号电机加速,2、3号电机减速(绕X轴旋转)
俯仰:1、2号电机加速,3、4号电机减速(绕Y轴旋转)
偏航:1、3号电机加速,2、4号电机减速(反扭矩差动)*/
if(rc_in->THROTTLE>180&&armed)//当油门大于150时和飞机解锁时动力分配才生效
{
Moto_PWM_1 = rc_in->THROTTLE + PID_ROL_Rate.OutPut + PID_PIT_Rate.OutPut + PID_YAW_Rate.OutPut;
Moto_PWM_2 = rc_in->THROTTLE - PID_ROL_Rate.OutPut + PID_PIT_Rate.OutPut - PID_YAW_Rate.OutPut;
Moto_PWM_3 = rc_in->THROTTLE - PID_ROL_Rate.OutPut - PID_PIT_Rate.OutPut + PID_YAW_Rate.OutPut;
Moto_PWM_4 = rc_in->THROTTLE + PID_ROL_Rate.OutPut - PID_PIT_Rate.OutPut - PID_YAW_Rate.OutPut;
}
else
{
Moto_PWM_1 = 0;
Moto_PWM_2 = 0;
Moto_PWM_3 = 0;
Moto_PWM_4 = 0;
}
Moto_Pwm(Moto_PWM_1,Moto_PWM_2,Moto_PWM_3,Moto_PWM_4); //将此数值分配到定时器,输出对应占空比的PWM波
}
/******************************************************************************************
* 函 数:int16_t Yaw_Control(float TARGET_YAW)
* 功 能:航向角不回中控制
* 参 数:TARGET_YAW 目标航向角
* 返回值:针对目标航向角计算出的航向角
* 备 注:由于遥控器航向舵会自动回中,所以需要对目标航向角进行不回中处理;
*******************************************************************************************/
int16_t Yaw_Control(float TARGET_YAW)
{
static int16_t YAW=0; //根据目标航向角计算出的不回中角度
if(Airplane_Enable)
{
if(SI24R1_Controlflag) //遥控器控制航向角
{
if(TARGET_YAW>2) //目标航向角为正时YAW增大
YAW +=2;
if(TARGET_YAW<-2) //目标航向角为负时YAW减小
YAW -=2;
}
}
return YAW;
}
/******************************************************************************************
* 函 数:void Yaw_Carefree(FLOAT_ANGLE *Target_Angle, const FLOAT_ANGLE *Measure_Angle)
* 功 能:无头角度控制(新手友好)
* 参 数:*Target_Angle 指向目标姿态角的指针
* *Measure_Angle 测量姿态角的指针
* 返回值:针对目标航向角计算出的航向角
* 备 注:无头模式需调用此函数
*******************************************************************************************/
void Yaw_Carefree(FLOAT_ANGLE *Target_Angle, const FLOAT_ANGLE *Measure_Angle)
{
float yawRad = fabs(Measure_Angle->yaw) * DegtoRad;
float cosy = cosf(yawRad);
float siny = sinf(yawRad);
float originalRoll = Target_Angle->rol;
float originalPitch = Target_Angle->pit;
// 将遥控器指令从世界坐标系转换到机体坐标系
// 无论飞机朝向如何,前进都是相对于起飞时的方向
Target_Angle->rol = originalRoll * cosy + originalPitch * siny;
Target_Angle->pit = originalPitch * cosy - originalRoll * siny;
}
/******************************************************************************************
* 函 数:void Safety_Check(void)
* 功 能:飞机姿态安全监测
* 参 数:无
* 返回值:无
* 备 注:如果飞机角度和加速度异常就将飞机上锁并停止电机,防止电机狂转打坏桨叶
*******************************************************************************************/
void Safety_Check(void)
{
if((fabs(Att_Angle.pit)>45.0f||fabs(Att_Angle.rol)>45.0f) && (fabs(Acc_filt.X)>9.0f||fabs(Acc_filt.Y)>9.0f))
{
Airplane_Enable = 0;
Moto_PWM_1 = 0;
Moto_PWM_2 = 0;
Moto_PWM_3 = 0;
Moto_PWM_4 = 0;
}
}
2.5.最后再在主函数 里调用一下使用到的函数即可,这里的摇杆控制信息就是在这个函数里被调用的Control(&Att_Angle,&Gyr_rad,&RC_Control,Airplane_Enable),这里主函数我只列出来使用到的部分了,具体代码可去我之前开源的四轴飞行器参考。
cs
NvicConfig(); //系统中断优先级管理
Delay_Init(); //系统延时初始化
TIM_Init(); //系统时基初始化
Exit_Init(); //外部中断初始化,SI24R1(2.4G)遥控器用
SI24R1_Init(); //SI24R1(2.4G)初始化(RGB红)
MOTOR_Init(); //电机PWM输出初始化
while(1)
{
Control(&Att_Angle,&Gyr_rad,&RC_Control,Airplane_Enable); //RC_Control就是摇杆数值
SI24R1_SingalCheck(); //2.4G通信检测
SendToRemote(); //发送数据给遥控器
SI24R1_GetAddr(); //分配2.4G地址
}
本次分享到这里就结束了,如果对你有帮助的话,希望能换你一个点赞!