adc.c /joystick.c /remoter.c 代码详解
(1)adc.c
adc.c 在HARDWARE分组下。
adc.c 主要实现采集摇杆电位器电压 AD 值。
①
//初始化ADC,使用DMA传输
//通道PA0\PA1\PA3\PA4
void Adc_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
ADC_InitTypeDef ADC_InitStructure;
DMA_InitTypeDef DMA_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_GPIOB,ENABLE);//使能GPIOA\B时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1,ENABLE);//使能ADC1时钟
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);//使能DMA时钟
//PA0\1\2 作为模拟通道输入引脚
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0|GPIO_Pin_1|GPIO_Pin_2;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN; //模拟输入引脚
GPIO_Init(GPIOA, &GPIO_InitStructure);
//PB0\1 作为模拟通道输入引脚
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0|GPIO_Pin_1;
GPIO_Init(GPIOB, &GPIO_InitStructure);
//DMA 配置
DMA_DeInit(DMA1_Channel1);
DMA_InitStructure.DMA_PeripheralBaseAddr = (u32)&ADC1->DR; //ADC1->DR地址
DMA_InitStructure.DMA_MemoryBaseAddr = (u32)&adc_value;//内存地址
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;
DMA_InitStructure.DMA_BufferSize = 5*ADC_SAMPLE_NUM;
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;//外设地址固定
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; //内存地址增加
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; //半字
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;
DMA_InitStructure.DMA_Mode = DMA_Mode_Circular; //循环传输
DMA_InitStructure.DMA_Priority = DMA_Priority_High;
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;
DMA_Init(DMA1_Channel1, &DMA_InitStructure);
DMA_Cmd(DMA1_Channel1, ENABLE);
ADC_DeInit(ADC1); //复位ADC1,将外设 ADC1 的全部寄存器重设为缺省值
ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; //ADC工作模式:ADC1和ADC2工作在独立模式
ADC_InitStructure.ADC_ScanConvMode = ENABLE; //扫描模式,用于多通道采集
ADC_InitStructure.ADC_ContinuousConvMode = ENABLE; //连续转换模式
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; //转换由软件而不是外部触发启动
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; //ADC数据右对齐
ADC_InitStructure.ADC_NbrOfChannel = 5; //顺序进行规则转换的ADC通道的数目
ADC_Init(ADC1, &ADC_InitStructure); //根据ADC_InitStruct中指定的参数初始化外设ADCx的寄存器
ADC_Cmd(ADC1, ENABLE); //使能指定的ADC1
ADC_DMACmd(ADC1, ENABLE);//使能ADC1 DMA
RCC_ADCCLKConfig(RCC_PCLK2_Div6); //设置ADC分频因子6 72M/6=12,ADC最大时间不能超过14M
//配置连续转换通道,55.5个采样周期
ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_239Cycles5); //1个通道转换一次耗时21us 4个通道
ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_239Cycles5); //采样个数ADC_SAMPLE_NUM
ADC_RegularChannelConfig(ADC1, ADC_Channel_2, 3, ADC_SampleTime_239Cycles5); //总共耗时4*21*ADC_SAMPLE_NUM(64)=5.4ms<10ms
ADC_RegularChannelConfig(ADC1, ADC_Channel_8, 4, ADC_SampleTime_239Cycles5);
ADC_RegularChannelConfig(ADC1, ADC_Channel_9, 5, ADC_SampleTime_239Cycles5);
ADC_ResetCalibration(ADC1); //使能复位校准
while(ADC_GetResetCalibrationStatus(ADC1)); //等待复位校准结束
ADC_StartCalibration(ADC1); //开启AD校准
while(ADC_GetCalibrationStatus(ADC1)); //等待校准结束
ADC_SoftwareStartConvCmd(ADC1, ENABLE); //使能指定的ADC1的软件转换启动功能
}
DMA(Direct Memory Access):直接存储器访问。
DMA传输将数据从一个地址空间复制到另一个地址空间。当CPU初始化这个动作后,传输动作本身是由DMA控制器来实现和完成。采用DMA传输数据不仅不会让处理器的工作延迟,反而可以解放处理器去处理其他事项。 DMA传输方式无需CPU直接控制传输,也没有中断处理方式那样保留现场和恢复现场的过程,通过硬件为RAM与I/O设备开辟一条直接传送数据的通路,能使CPU得效率大为提高。 STM32最多有2个DMA控制器,DMA1有7个通道。DMA2有5个通道。每个通道专门用来管理来自于一个或多个外设对存储器访问的请求。还有一个仲裁来协调各个DMA请求的优先权。
②
#define ADC_SAMPLE_NUM 10
u16 adc_value[5*ADC_SAMPLE_NUM];//ADC采集值存放缓冲区
//ADC均值滤波
void ADC_Filter(u16* adc_val)
{
u16 i=0;
u32 sum[5]={0,0,0,0};
for(;i<ADC_SAMPLE_NUM;i++)
{
sum[0]+=adc_value[5*i+0];//adc_value是ADC采集出来的数据
sum[1]+=adc_value[5*i+1];
sum[2]+=adc_value[5*i+2];
sum[3]+=adc_value[5*i+3];
sum[4]+=adc_value[5*i+4];
}
adc_val[0]=sum[0]/ADC_SAMPLE_NUM;
adc_val[1]=sum[1]/ADC_SAMPLE_NUM;
adc_val[2]=sum[2]/ADC_SAMPLE_NUM;
adc_val[3]=sum[3]/ADC_SAMPLE_NUM;
adc_val[4]=sum[4]/ADC_SAMPLE_NUM;
}
③
u16 getAdcValue(u8 axis)
{
u32 sum=0;
for(u8 i=0;i<ADC_SAMPLE_NUM;i++)
{
sum += adc_value[5*i+axis];
}
return sum/ADC_SAMPLE_NUM;
}
(2)joystick.c
joystick.c在COMMUNICATE分组下。
joystick.c 主要实现将 AD值转为 THRUST、YAW、PITCH、ROLL 对应百分比。
joystick—摇杆,THRUST—油门值
YAW—偏航角,PITCH—俯仰角,ROLL—滚转角
①
/*摇杆初始化*/
void joystickInit(void)
{
if(isInit) return;
Adc_Init();
jsParam = &configParam.jsParam;
isInit = true;
}
②
/*获取摇杆ADC值*/
void getFlyDataADCValue(joystickFlyui16_t *adcValue)
{
adcValue->thrust = getAdcValue(ADC_THRUST);
adcValue->roll = getAdcValue(ADC_ROLL);
adcValue->pitch = getAdcValue(ADC_PITCH);
adcValue->yaw = getAdcValue(ADC_YAW);
}
③
/*ADC值转换成飞控数据百分比*/
void ADCtoFlyDataPercent(joystickFlyf_t *percent)
{
s16 adcValue;
//THRUST
adcValue = getAdcValue(ADC_THRUST) - jsParam->thrust.mid;
adcValue = deadband(adcValue,MID_DB_THRUST);
if(adcValue>=0)
percent->thrust = (float)adcValue/(jsParam->thrust.range_pos-MID_DB_THRUST-DB_RANGE);
else
percent->thrust = (float)adcValue/(jsParam->thrust.range_neg-MID_DB_THRUST-DB_RANGE);
//ROLL
adcValue = getAdcValue(ADC_ROLL) - jsParam->roll.mid;
adcValue = deadband(adcValue, MID_DB_ROLL);
if(adcValue >= 0)
percent->roll = (float)adcValue/(jsParam->roll.range_pos-MID_DB_ROLL-DB_RANGE);
else
percent->roll = (float)adcValue/(jsParam->roll.range_neg-MID_DB_ROLL-DB_RANGE);
//PITCH
adcValue = getAdcValue(ADC_PITCH) - jsParam->pitch.mid;
adcValue = deadband(adcValue, MID_DB_PITCH);
if(adcValue >= 0)
percent->pitch = (float)adcValue/(jsParam->pitch.range_pos-MID_DB_PITCH-DB_RANGE);
else
percent->pitch = (float)adcValue/(jsParam->pitch.range_neg-MID_DB_PITCH-DB_RANGE);
//YAW
adcValue = getAdcValue(ADC_YAW) - jsParam->yaw.mid;
adcValue = deadband(adcValue, MID_DB_YAW);
if(adcValue >= 0)
percent->yaw = (float)adcValue/(jsParam->yaw.range_pos-MID_DB_YAW-DB_RANGE);
else
percent->yaw = (float)adcValue/(jsParam->yaw.range_neg-MID_DB_YAW-DB_RANGE);
}
(3)remoter.c
remoter.c在COMMUNICATE分组下。
remoter.c 主要实现将百分比乘以设定速度值并打包成 ATKP 包格式,然后以 10ms 周期性发送到 radiolink.c 的发送队列中,即 commanderTask。
/*发送飞控命令任务*/
void commanderTask(void* param)
{
float max_thrust = LOW_SPEED_THRUST;
float max_pitch = LOW_SPEED_PITCH;
float max_roll = LOW_SPEED_ROLL;
joystickFlyf_t percent;
while(1)
{
vTaskDelay(10);
switch(configParam.flight.speed)
{
case LOW_SPEED:
max_thrust = LOW_SPEED_THRUST;
max_pitch = LOW_SPEED_PITCH;
max_roll = LOW_SPEED_ROLL;
break;
case MID_SPEED:
max_thrust = MID_SPEED_THRUST;
max_pitch = MID_SPEED_PITCH;
max_roll = MID_SPEED_ROLL;
break;
case HIGH_SPEED:
max_thrust = HIGH_SPEED_THRUST;
max_pitch = HIGH_SPEED_PITCH;
max_roll = HIGH_SPEED_ROLL;
break;
}
ADCtoFlyDataPercent(&percent);
//THRUST
if(configParam.flight.ctrl == ALTHOLD_MODE || configParam.flight.ctrl == THREEHOLD_MODE)/*定高模式 和定点模式*/
{
flydata.thrust = percent.thrust * ALT_THRUST;
flydata.thrust += ALT_THRUST;
flydata.thrust = limit(flydata.thrust, 0, 100);
}
else
{
flydata.thrust = percent.thrust * (max_thrust - MIN_THRUST);
flydata.thrust += MIN_THRUST;
flydata.thrust = limit(flydata.thrust, MIN_THRUST, max_thrust);
}
//ROLL
flydata.roll = percent.roll * max_roll;
flydata.roll = limit(flydata.roll, -max_roll, max_roll);
//PITCH
flydata.pitch = percent.pitch * max_pitch;
flydata.pitch = limit(flydata.pitch, -max_pitch, max_pitch);
//YAW
flydata.yaw = percent.yaw * MAX_YAW;
flydata.yaw = limit(flydata.yaw, -MAX_YAW, MAX_YAW);
/*发送飞控数据*/
if(getRCLock()==false && radioinkConnectStatus()==true && getIsMFCanFly()==true)
{
remoterData_t send;
switch(configParam.flight.mode)
{
case HEAD_LESS:
send.flightMode = 1;
break;
case X_MODE:
send.flightMode = 0;
break;
}
switch(configParam.flight.ctrl)
{
case ALTHOLD_MODE:
send.ctrlMode = 1;
break;
case MANUAL_MODE:
send.ctrlMode = 0;
break;
case THREEHOLD_MODE:
send.ctrlMode = 3;
break;
}
if(flydata.thrust<=MIN_THRUST && send.ctrlMode==0)
{
send.thrust = 0;
}
else
{
send.thrust = flydata.thrust;
}
if(getTrimFlag() == true)
{
send.pitch = 0;
send.roll = 0;
}
else
{
send.pitch = flydata.pitch ;
send.roll = flydata.roll;
}
send.yaw = flydata.yaw;
send.trimPitch = configParam.trim.pitch;
send.trimRoll = configParam.trim.roll;
/*发送飞控数据*/
sendRmotorData((u8*)&send, sizeof(send));
}
/*发送遥感数据至匿名上位机*/
if(radioinkConnectStatus()==true)
{
atkp_t p;
joystickFlyui16_t rcdata;
rcdata.thrust = flydata.thrust*10 + 1000;
rcdata.pitch = percent.pitch*500 + 1500;
rcdata.roll = percent.roll*500 + 1500;
rcdata.yaw = percent.yaw*500 + 1500;
p.msgID = DOWN_RCDATA;
p.dataLen = sizeof(rcdata);
memcpy(p.data, &rcdata, p.dataLen);
radiolinkSendPacket(&p);
}
}
}
其中,
① 油门值 THRUST:
定高模式和定点模式下
#define ALT_THRUST (50.0)
flydata.thrust = percent.thrust * ALT_THRUST;
flydata.thrust += ALT_THRUST;
flydata.thrust = limit(flydata.thrust, 0, 100);
手动模式下
flydata.thrust = percent.thrust * (max_thrust - MIN_THRUST);
flydata.thrust += MIN_THRUST;
flydata.thrust = limit(flydata.thrust, MIN_THRUST, max_thrust);
② 滚转角 ROLL:
flydata.roll = percent.roll * max_roll;
flydata.roll = limit(flydata.roll, -max_roll, max_roll);
③ 俯仰角 PITCH:
flydata.pitch = percent.pitch * max_pitch;
flydata.pitch = limit(flydata.pitch, -max_pitch, max_pitch);
④ 偏航角 YAW:
#define MAX_YAW (200.0)
flydata.yaw = percent.yaw * MAX_YAW;
flydata.yaw = limit(flydata.yaw, -MAX_YAW, MAX_YAW);
任务创建与调度
任务创建代码如下:
/*创建任务*/
void startTask(void *param)
{
taskENTER_CRITICAL(); /*进入临界区*/
xTaskCreate(radiolinkTask, "RADIOLINK", 100, NULL, 6, &radiolinkTaskHandle);/*创建无线连接任务*/
xTaskCreate(usblinkTxTask, "USBLINK_TX", 100, NULL, 5, NULL); /*创建usb发送任务*/
xTaskCreate(usblinkRxTask, "USBLINK_RX", 100, NULL, 5, NULL); /*创建usb接收任务*/
xTaskCreate(commanderTask, "COMMANDER", 100, NULL, 4, NULL); /*创建飞控指令发送任务*/
xTaskCreate(keyTask, "BUTTON_SCAN", 100, NULL, 3, NULL); /*创建按键扫描任务*/
xTaskCreate(displayTask, "DISPLAY", 200, NULL, 1, NULL); /*创建显示任务*/
xTaskCreate(configParamTask, "CONFIG_TASK", 100, NULL, 1, NULL);/*创建参数配置任务*/
xTaskCreate(radiolinkDataProcessTask, "DATA_PROCESS", 100, NULL, 6, NULL); /*创建无线通信数据处理任务*/
xTaskCreate(usblinkDataProcessTask, "DATA_PROCESS", 100, NULL, 6, NULL); /*创建USB通信数据处理任务*/
vTaskDelete(startTaskHandle); /*删除开始任务*/
taskEXIT_CRITICAL(); /*退出临界区*/
}
其中,xTaskCreate函数参数列表为:
xTaskCreate(TaskFunction_t pxTaskCode,
const char * const pcName,
const uint16_t usStackDepth,
void * const pvParameters,
UBaseType_t uxPriority,
TaskHandle_t * const pxCreatedTask )
main.c的主函数中创建起始任务与开启任务调度:
xTaskCreate(startTask, "START_TASK", 100, NULL, 2, &startTaskHandle);/*创建起始任务*/
vTaskStartScheduler();/*开启任务调度*/
while(1){};/* 任务调度后不会执行到这 */
任务调度是操作系统的重要组成部分,而对于实时操作系统,任务调度直接影响其实时性能。
任务调度方式常规可分为:
可打断调度(实时系统基本功能):关键防止优先级倒置 ;
不可打断调度:先来先服务,不可中断。
任务调度算法可分为——事件驱动调度算法:根据事件的先后以及任务的优先级安排任务的执行;时钟驱动调度算法:一般用于周期任务。
事件驱动调度依赖外部硬件设备,通过产生中断方式为任务调度提供信号。分两种,集成事件驱动调度:中断的优先级与任务的优先级相对应,中断只有在其优先级高于正在执行的任务时才会被处理器响应。 非集成事件驱动调度:任务通过外部中断启动,中断优先级与相关任务优先级没有关系。
嵌入式实时操作系统
嵌入式实时操作系统(Embedded Real-time Operation System,RTOS)。嵌入式系统是“用于控制、监视或者辅助操作机器和设备的装置”。
当外界事件或数据产生时,能够接受并以足够快的速度予以处理,其处理的结果又能在规定的时间之内来控制生产过程或对处理系统作出快速响应,并控制所有实时任务协调一致运行的嵌入式操作系统。
FreeRTOS 是一个 RTOS 类的嵌入式实时操作系统。
操作系统允许多个任务同时运行,这个叫做多任务,实际上,一个处理器核心在某一时刻只能运行一个任务。操作系统中任务调度器的责任就是决定在某一时刻究竟运行哪个任务,任务调度在各个任务之间的切换非常快!这就给人们造成了同一时刻有多个任务同时运行的错觉。
RTOS 的任务调度器被设计为可预测的,而这正是嵌入式实时操作系统所需要的,实时环境中要求操作系统必须对某一个事件做出实时的响应,因此系统任务调度器的行为必须是可预测的。像FreeRTOS 这种传统的 RTOS 类操作系统是由用户给每个任务分配一个任务优先级,任务调度器就可以根据此优先级来决定下一刻应该运行哪个任务。
例程代码是基于 FreeRTOS 操作系统的。
FreeRTOS 的文件数量很少。
FreeRTOS 被移植到了很多不同的微处理器上,比如我们使用的 STM32,F1、F3、F4 和最新的 F7 都有移植,这个极大的方便了我们学习和使用。
高可移植性,代码主要 C 语言编写。
FreeRTOS-MPU 支持 Corex-M 系列中的 MPU 单元,如 STM32F103。
支持实时任务和协程。
任务与任务、任务与中断之间可以使用任务通知、消息队列、二值信号量、数值型信号量、递归互斥信号量和互斥信号量进行通信和同步。
任务数量不限。
任务优先级不限。
FreeRTOS 任务基础知识
RTOS 系统的核心就是任务管理。
(1)多任务系统
FreeRTOS 是一个抢占式的实时多任务系统,那么其任务调度器也是抢占式的,运行过程如图所示:
高优先级的任务可以打断低优先级任务的运行而取得 CPU 的使用权,这样就保证了那些紧急任务的运行。
高优先级的任务执行完成以后重新把 CPU 的使用权归还给低优先级的任务,这个就是抢占式多任务系统的基本原理。
(2)FreeRTOS 任务与协程
任务(Task)
在使用 RTOS 的时候一个实时应用可以作为一个独立的任务。每个任务都有自己的运行环境,不依赖于系统中其他的任务或者 RTOS 调度器。任何一个时间点只能有一个任务运行,具体运行哪个任务是由 RTOS 调度器来决定的,RTOS 调度器因此就会重复的开启、关闭每个任务。任务不需要了解 RTOS 调度器的具体行为,RTOS 调度器的职责是确保当一个任务开始执行的时候其上下文环境(寄存器值,堆栈内容等)和任务上一次退出的时候相同。为了做到这一点,每个任务都必须有个堆栈,当任务切换的时候将上下文环境保存在堆栈中,这样当任务再次执行的时候就可以从堆栈中取出上下文环境,任务恢复运行。
任务特性:
1、简单。
2、没有使用限制。 3、支持抢占
4、支持优先级
5、每个任务都拥有堆栈导致了 RAM 使用量增大。
6、如果使用抢占的话的必须仔细的考虑重入的问题。
协程(Co-routine)
协程是为那些资源很少的 MCU 而做的,但是随着 MCU 的飞速发展,性能越来越强大,现
在协程几乎很少用到了。为了降低对 RAM 的消耗做了很多的限制。
所有的协程使用同一个堆栈(如果是任务的话每个任务都有自己的堆栈),这样就比使用任
务消耗更少的 RAM。
(3)任务状态
(4)任务优先级
优先级数字越低表示任务的优先级越低,0 的优先级最低,configMAX_PRIORITIES-1 的优先级最高。空闲任务的优先级最低,为 0。
FreeRTOS 调度器确保处于就绪态或运行态的高优先级的任务获取处理器使用权,换句话说就是处于就绪态的最高优先级的任务才会运行。当宏 configUSE_TIME_SLICING 定义为 1 的时候多个任务可以共用一个优先级,数量不限。默认情况下宏 configUSE_TIME_SLICING 在文件FreeRTOS.h 中已经定义为 1。此时处于就绪态的优先级相同的任务就会使用时间片轮转调度器获取运行时间。
时间片轮转调度:
每个进程被分配一个时间段,称作它的时间片,即该进程允许运行的时间。如果在时间片结束时进程还在运行,则CPU将被剥夺并分配给另一个进程。如果进程在时间片结束前阻塞或结束,则CPU当即进行切换。调度程序所要做的就是维护一张就绪进程列表,当进程用完它的时间片后,它被移到队列的末尾。
(5)任务实现
在使用 FreeRTOS 的过程中,我们要使用函数 xTaskCreate()或 xTaskCreateStatic()来创建任务,这两个函数的第一个参数 pxTaskCode,就是这个任务的任务函数。
任务函数就是完成本任务工作的函数。
任务函数模板如下:
void vATaskFunction(void *pvParameters)
{
for( ; ; )
{
--任务应用程序--
vTaskDelay();
}
/* 不能从任务函数中返回或者退出, 从 任 务 函 数 中 返 回 或 退 出 的 话 就 会 调 用
configASSERT(),前提是你定义了 configASSERT()。如果一定要从任务函数中退出的话那一定
要调用函数 vTaskDelete(NULL)来删除此任务。*/
vTaskDelete(NULL);
}
(6)任务控制块
FreeRTOS 的每个任务都有一些属性需要存储,FreeRTOS 把这些属性集合到一起用一个结构体来表示,这个结构体叫做任务控制块:TCB_t,在使用函数 xTaskCreate()创建任务的时候就会自动的给每个任务分配一个任务控制块。
(7)任务堆栈
FreeRTOS 之所以能正确的恢复一个任务的运行就是因为有任务堆栈在保驾护航,任务调度器在进行任务切换的时候会将当前任务的现场(CPU 寄存器值等)保存在此任务的任务堆栈中,等到此任务下次运行的时候就会先用堆栈中保存的值来恢复现场,恢复现场以后任务就会接着从上次中断的地方开始运行。
创建任务的时候需要给任务指定堆栈,如果使用的函数 xTaskCreate()创建任务(动态方法)的话那么任务堆栈就会由函数 xTaskCreate()自动创建。