大家好,欢迎来到IT知识分享网。
BetaFlight开源飞控源码分析
1.整体框架
sudo apt install tree tree -a
betaflight ├──> src/main //源代码&引用头文件目录 │ ├──> main.c //C代码入口main() │ ├──> ... //省略很多模块,后续会深入分析,比如:blackbox,osd等 │ ├──> drivers //各类硬件芯片驱动 │ ├──> target //各类目标板(airframe):代码+配置 │ │ └──> STM32F405 //举例:STM32F405 │ │ ├──> target.c │ │ ├──> target.h │ │ └──> target.mk │ └──> startup │ └──> startup_stm32f745xx.s ├──> obj //编译目标文件目录 │ ├──> main │ └──> betaflight_4.4.0_STM32F405.hex ├──> lib/main //库代码目录 │ ├──> MAVLink │ ├──> CMSIS │ ├──> STM32_USB-FS-Device_Driver │ ├──> FatFS │ └──> STM32F7 ├──> src/link //链接脚本目录 │ ├──> stm32_flash_f7_split.ld │ └──> stm32_flash_f74x.ld ├──> make //通用makefile目录 ├──> tools //本地工具安装,比如:gcc-arm-none-eabi-9-2020-q2-update └──> downloads //下载目录,比如:下载的工具链压缩包gcc-arm-none-eabi-9-2020-q2-update-x86_64-linux.tar.bz2
2.主函数源码分析
src是整个代码的核心文件夹,打开betaflight\src\main\main.c,可以看到betaflight是用C语言开发的一款飞控项目,其入口也是从main()函数开始。
main ├──> init() //硬件初始化 └──> run() //任务循环运行
整体上分为两部分:初始化和运行。
2.1.初始化
init ├──> <SERIAL_PORT_COUNT> printfSerialInit ├──> systemInit ├──> tasksInitData ├──> IOInitGlobal ├──> <USE_HARDWARE_REVISION_DETECTION> detectHardwareRevision ├──> <USE_TARGET_CONFIG> targetConfiguration ├──> pgResetAll ├──> <USE_SDCARD_SPI> configureSPIBusses(); initFlags |= SPI_BUSSES_INIT_ATTEMPTED; ├──> sdCardAndFSInit; initFlags |= SD_INIT_ATTEMPTED; ├──> <!sdcard_isInserted()> failureMode(FAILURE_SDCARD_REQUIRED) ├──> [SD Card FS check] //while (afatfs_getFilesystemState() != AFATFS_FILESYSTEM_STATE_READY) │ ├──> afatfs_poll() │ └──> <afatfs_getFilesystemState() == AFATFS_FILESYSTEM_STATE_FATAL> failureMode(FAILURE_SDCARD_INITIALISATION_FAILED) ├──> <CONFIG_IN_EXTERNAL_FLASH> || <CONFIG_IN_MEMORY_MAPPED_FLASH)> │ ├──> pgResetAll() │ ├──> <CONFIG_IN_EXTERNAL_FLASH> configureSPIBusses(); initFlags |= SPI_BUSSES_INIT_ATTEMPTED; │ ├──> configureQuadSPIBusses();configureOctoSPIBusses();initFlags |= QUAD_OCTO_SPI_BUSSES_INIT_ATTEMPTED; │ ├──> bool haveFlash = flashInit(flashConfig()); │ ├──> <!haveFlash>failureMode(FAILURE_EXTERNAL_FLASH_INIT_FAILED) │ └──> initFlags |= FLASH_INIT_ATTEMPTED ├──> initEEPROM ├──> ensureEEPROMStructureIsValid ├──> bool readSuccess = readEEPROM() ├──> <USE_BOARD_INFO> initBoardInformation ├──> <!readSuccess || !isEEPROMVersionValid() || strncasecmp(systemConfig()->boardIdentifier, TARGET_BOARD_IDENTIFIER, sizeof(TARGET_BOARD_IDENTIFIER))> resetEEPROM() ├──> systemState |= SYSTEM_STATE_CONFIG_LOADED ├──> <USE_DEBUG_PIN> dbgPinInit ├──> debugMode = systemConfig()->debug_mode ├──> <TARGET_PREINIT> targetPreInit ├──> <!defined(USE_VIRTUAL_LED)> ledInit(statusLedConfig()) ├──> <!defined(SIMULATOR_BUILD)> EXTIInit ├──> <USE_BUTTONS> │ ├──> buttonsInit │ └──> <EEPROM_RESET_PRECONDITION && defined(BUTTON_A_PIN) && defined(BUTTON_B_PIN)> //#define EEPROM_RESET_PRECONDITION (!isMPUSoftReset()) │ └──> resetEEPROM/systemReset ├──> <defined(STM32F4) || defined(STM32G4)> // F4 has non-8MHz boards, G4 for Betaflight allow 24 or 27MHz oscillator │ └──> systemClockSetHSEValue(systemConfig()->hseMhz * U) ├──> <USE_OVERCLOCK> OverclockRebootIfNecessary(systemConfig()->cpu_overclock) ├──> <USE_MCO> │ ├──> <defined(STM32F4) || defined(STM32F7)> │ │ └──> mcoConfigure(MCODEV_2, mcoConfig(MCODEV_2)); │ └──> <defined(STM32G4)> │ └──> mcoConfigure(MCODEV_1, mcoConfig(MCODEV_1)); ├──> <USE_TIMER> timerInit ├──> <BUS_SWITCH_PIN> busSwitchInit ├──> <defined(USE_UART) && !defined(SIMULATOR_BUILD)> uartPinConfigure(serialPinConfig()) ├──> [serialInit] │ ├──> <AVOID_UART1_FOR_PWM_PPM> serialInit(featureIsEnabled(FEATURE_SOFTSERIAL), featureIsEnabled(FEATURE_RX_PPM) || featureIsEnabled(FEATURE_RX_PARALLEL_PWM) ? SERIAL_PORT_USART1 : SERIAL_PORT_NONE); │ ├──> <AVOID_UART2_FOR_PWM_PPM> serialInit(featureIsEnabled(FEATURE_SOFTSERIAL), featureIsEnabled(FEATURE_RX_PPM) || featureIsEnabled(FEATURE_RX_PARALLEL_PWM) ? SERIAL_PORT_USART2 : SERIAL_PORT_NONE); │ ├──> <AVOID_UART3_FOR_PWM_PPM> serialInit(featureIsEnabled(FEATURE_SOFTSERIAL), featureIsEnabled(FEATURE_RX_PPM) || featureIsEnabled(FEATURE_RX_PARALLEL_PWM) ? SERIAL_PORT_USART3 : SERIAL_PORT_NONE); │ └──> <else> serialInit(featureIsEnabled(FEATURE_SOFTSERIAL), SERIAL_PORT_NONE) ├──> mixerInit(mixerConfig()->mixerMode) ├──> uint16_t idlePulse = motorConfig()->mincommand ├──> <featureIsEnabled(FEATURE_3D)> idlePulse = flight3DConfig()->neutral3d ├──> <motorConfig()->dev.motorPwmProtocol == PWM_TYPE_BRUSHED> idlePulse = 0; // brushed motors ├──> <USE_MOTOR> motorDevInit(&motorConfig()->dev, idlePulse, getMotorCount()); systemState |= SYSTEM_STATE_MOTORS_READY ├──> <USE_RX_PPM> <featureIsEnabled(FEATURE_RX_PARALLEL_PWM)> pwmRxInit(pwmConfig()) ├──> <USE_BEEPER> beeperInit(beeperDevConfig()) ├──> <defined(USE_INVERTER) && !defined(SIMULATOR_BUILD)> initInverters(serialPinConfig()) ├──> [Hardware Bus Initialization] │ ├──> <TARGET_BUS_INIT> targetBusInit() │ └──> <else> │ ├──> <!(initFlags & SPI_BUSSES_INIT_ATTEMPTED)> configureSPIBusses();initFlags |= SPI_BUSSES_INIT_ATTEMPTED; │ ├──> <!(initFlags & QUAD_OCTO_SPI_BUSSES_INIT_ATTEMPTED)> configureQuadSPIBusses();configureOctoSPIBusses();initFlags |= QUAD_OCTO_SPI_BUSSES_INIT_ATTEMPTED; │ ├──> <defined(USE_SDCARD_SDIO) && !defined(CONFIG_IN_SDCARD) && defined(STM32H7)> sdioPinConfigure(); SDIO_GPIO_Init(); │ ├──> <USE_USB_MSC> │ │ ├──> mscInit() │ │ └──> <USE_SDCARD> <blackboxConfig()->device == BLACKBOX_DEVICE_SDCARD> <sdcardConfig()->mode> <!(initFlags & SD_INIT_ATTEMPTED)> sdCardAndFSInit();initFlags |= SD_INIT_ATTEMPTED; │ ├──> <USE_FLASHFS> <blackboxConfig()->device == BLACKBOX_DEVICE_FLASH> emfat_init_files │ ├──> <USE_SPI> spiInitBusDMA │ ├──> <mscStart() == 0> mscWaitForButton(); │ ├──> <mscStart() != 0> systemResetFromMsc() │ ├──> <USE_PERSISTENT_MSC_RTC> │ │ ├──> persistentObjectWrite(PERSISTENT_OBJECT_RTC_HIGH, 0); │ │ └──> persistentObjectWrite(PERSISTENT_OBJECT_RTC_LOW, 0); │ └──> <USE_I2C> │ ├──> i2cHardwareConfigure(i2cConfig(0)); │ ├──> <USE_I2C_DEVICE_1> i2cInit(I2CDEV_1); │ ├──> <USE_I2C_DEVICE_2> i2cInit(I2CDEV_2); │ ├──> <USE_I2C_DEVICE_3> i2cInit(I2CDEV_3); │ └──> <USE_I2C_DEVICE_4> i2cInit(I2CDEV_4); ├──> <USE_HARDWARE_REVISION_DETECTION> updateHardwareRevision ├──> <USE_VTX_RTC6705> bool useRTC6705 = rtc6705IOInit(vtxIOConfig()); ├──> <USE_CAMERA_CONTROL> cameraControlInit(); ├──> <USE_ADC> adcInit(adcConfig()); ├──> initBoardAlignment(boardAlignment()); ├──> <!sensorsAutodetect()> │ ├──> <isSystemConfigured()> │ │ └──> indicateFailure(FAILURE_MISSING_ACC, 2); │ └──> setArmingDisabled(ARMING_DISABLED_NO_GYRO); ├──> systemState |= SYSTEM_STATE_SENSORS_READY; ├──> gyroSetTargetLooptime(pidConfig()->pid_process_denom); // Set the targetLooptime based on the detected gyro sampleRateHz and pid_process_denom ├──> validateAndFixGyroConfig(); ├──> gyroSetTargetLooptime(pidConfig()->pid_process_denom); // Now reset the targetLooptime as it's possible for the validation to change the pid_process_denom ├──> gyroInitFilters(); ├──> pidInit(currentPidProfile); ├──> mixerInitProfile(); ├──> <USE_PID_AUDIO> pidAudioInit(); ├──> <USE_SERVOS> │ ├──> servosInit(); │ ├──> <isMixerUsingServos()> servoDevInit(&servoConfig()->dev) //pwm_params.useChannelForwarding = featureIsEnabled(FEATURE_CHANNEL_FORWARDING); │ └──> servosFilterInit(); ├──> <USE_PINIO> pinioInit(pinioConfig()); ├──> <USE_PIN_PULL_UP_DOWN> pinPullupPulldownInit(); ├──> <USE_PINIOBOX> pinioBoxInit(pinioBoxConfig()); ├──> [LED Oprations] ├──> imuInit(); ├──> failsafeInit(); ├──> rxInit(); ├──> <USE_GPS> <featureIsEnabled(FEATURE_GPS)> │ ├──> gpsInit(); │ └──> <USE_GPS_RESCUE> gpsRescueInit(); ├──> <USE_LED_STRIP> │ ├──> ledStripInit(); │ └──> <featureIsEnabled(FEATURE_LED_STRIP)> ledStripEnable(); ├──> <USE_ESC_SENSOR> <featureIsEnabled(FEATURE_ESC_SENSOR)> escSensorInit(); ├──> <USE_USB_DETECT> usbCableDetectInit(); ├──> <USE_TRANSPONDER> <featureIsEnabled(FEATURE_TRANSPONDER)> │ ├──> transponderInit(); │ ├──> transponderStartRepeating(); │ └──> systemState |= SYSTEM_STATE_TRANSPONDER_ENABLED; ├──> <USE_FLASH_CHIP> <!(initFlags & FLASH_INIT_ATTEMPTED)> │ └──> flashInit(flashConfig());initFlags |= FLASH_INIT_ATTEMPTED; ├──> <USE_FLASHFS> flashfsInit(); ├──> <USE_SDCARD> <dcardConfig()->mode> <!(initFlags & SD_INIT_ATTEMPTED)> │ └──> sdCardAndFSInit();initFlags |= SD_INIT_ATTEMPTED; ├──> <USE_BLACKBOX> blackboxInit(); ├──> <USE_ACC> <mixerConfig()->mixerMode == MIXER_GIMBAL> accStartCalibration(); ├──> gyroStartCalibration(false); ├──> <USE_BARO> baroStartCalibration(); ├──> positionInit(); ├──> <defined(USE_VTX_COMMON) || defined(USE_VTX_CONTROL)> vtxTableInit(); ├──> <USE_VTX_CONTROL> │ ├──> vtxControlInit(); │ ├──> <USE_VTX_COMMON> vtxCommonInit(); │ ├──> <USE_VTX_MSP> vtxMspInit(); │ ├──> <USE_VTX_SMARTAUDIO> vtxSmartAudioInit(); │ ├──> <USE_VTX_TRAMP> vtxTrampInit(); │ └──> <USE_VTX_RTC6705> <!vtxCommonDevice() && useRTC6705> vtxRTC6705Init(); ├──> <USE_TIMER> timerStart(); ├──> batteryInit(); // always needs doing, regardless of features. ├──> <USE_RCDEVICE> rcdeviceInit(); ├──> <USE_PERSISTENT_STATS> statsInit(); ├──> [Initialize MSP] │ ├──> mspInit(); │ └──> mspSerialInit(); ├──> <USE_CMS> cmsInit(); ├──> <defined(USE_OSD) || (defined(USE_MSP_DISPLAYPORT) && defined(USE_CMS))> displayPort_t *osdDisplayPort = NULL; ├──> <USE_OSD> │ ├──> osdDisplayPortDevice_e osdDisplayPortDevice = OSD_DISPLAYPORT_DEVICE_NONE; │ └──> <featureIsEnabled(FEATURE_OSD)> │ ├──> osdDisplayPortDevice_e device = osdConfig()->displayPortDevice; │ ├──> <case OSD_DISPLAYPORT_DEVICE_AUTO:> FALLTHROUGH; │ ├──> <case OSD_DISPLAYPORT_DEVICE_FRSKYOSD:> │ │ ├──> osdDisplayPort = frskyOsdDisplayPortInit(vcdProfile()->video_system); │ │ └──> <osdDisplayPort || device == OSD_DISPLAYPORT_DEVICE_FRSKYOSD> osdDisplayPortDevice = OSD_DISPLAYPORT_DEVICE_FRSKYOSD; break; │ ├──> <case OSD_DISPLAYPORT_DEVICE_MAX7456:> │ │ └──> <max7456DisplayPortInit(vcdProfile(), &osdDisplayPort) || device == OSD_DISPLAYPORT_DEVICE_MAX7456> osdDisplayPortDevice = OSD_DISPLAYPORT_DEVICE_MAX7456;break; │ ├──> <case OSD_DISPLAYPORT_DEVICE_MSP:> │ │ ├──> osdDisplayPort = displayPortMspInit(); │ │ └──> <osdDisplayPort || device == OSD_DISPLAYPORT_DEVICE_MSP> osdDisplayPortDevice = OSD_DISPLAYPORT_DEVICE_MSP; break; │ ├──> <case OSD_DISPLAYPORT_DEVICE_NONE:) default: │ ├──> osdInit(osdDisplayPort, osdDisplayPortDevice); │ └──> <osdDisplayPortDevice == OSD_DISPLAYPORT_DEVICE_NONE> featureDisableImmediate(FEATURE_OSD); ├──> <defined(USE_CMS) && defined(USE_MSP_DISPLAYPORT)> <!osdDisplayPort> cmsDisplayPortRegister(displayPortMspInit()); ├──> <USE_DASHBOARD> <featureIsEnabled(FEATURE_DASHBOARD)> │ ├──> dashboardInit(); │ ├──> <USE_OLED_GPS_DEBUG_PAGE_ONLY> dashboardShowFixedPage(PAGE_GPS); │ └──> <!USE_OLED_GPS_DEBUG_PAGE_ONLY> dashboardResetPageCycling();dashboardEnablePageCycling(); ├──> <USE_TELEMETRY> <featureIsEnabled(FEATURE_TELEMETRY)> telemetryInit(); ├──> setArmingDisabled(ARMING_DISABLED_BOOT_GRACE_TIME); ├──> <defined(USE_SPI) && defined(USE_SPI_DMA_ENABLE_EARLY)> spiInitBusDMA(); ├──> <USE_MOTOR> │ ├──> motorPostInit(); │ └──> motorEnable(); ├──> <defined(USE_SPI) && defined(USE_SPI_DMA_ENABLE_LATE) && !defined(USE_SPI_DMA_ENABLE_EARLY)> │ └──> spiInitBusDMA(); ├──> debugInit(); ├──> unusedPinsInit(); ├──> tasksInit(); └──> systemState |= SYSTEM_STATE_READY;
初始化可以分成两大部分:
应用层的初始化通常是在对应的驱动层初始化之后。
2.2.运行
始化完成以后就执行run函数,它使用一个特殊的任务调度器管理各个运行的任务。
run └──> loop: scheduler()
2.3. 任务的创建
像FreeRTOS一样,需要创建好任务才会启用任务调度器,打开 src\main\fc\tasks.c,可以看到有关任务的结构体:
// Task ID data in .data (initialised data) task_attribute_t task_attributes[TASK_COUNT] = {
[TASK_SYSTEM] = DEFINE_TASK("SYSTEM", "LOAD", NULL, taskSystemLoad, TASK_PERIOD_HZ(10), TASK_PRIORITY_MEDIUM_HIGH), [TASK_MAIN] = DEFINE_TASK("SYSTEM", "UPDATE", NULL, taskMain, TASK_PERIOD_HZ(1000), TASK_PRIORITY_MEDIUM_HIGH), [TASK_SERIAL] = DEFINE_TASK("SERIAL", NULL, NULL, taskHandleSerial, TASK_PERIOD_HZ(100), TASK_PRIORITY_LOW), // 100 Hz should be enough to flush up to 115 bytes @ baud [TASK_BATTERY_ALERTS] = DEFINE_TASK("BATTERY_ALERTS", NULL, NULL, taskBatteryAlerts, TASK_PERIOD_HZ(5), TASK_PRIORITY_MEDIUM), [TASK_BATTERY_VOLTAGE] = DEFINE_TASK("BATTERY_VOLTAGE", NULL, NULL, batteryUpdateVoltage, TASK_PERIOD_HZ(SLOW_VOLTAGE_TASK_FREQ_HZ), TASK_PRIORITY_MEDIUM), // Freq may be updated in tasksInit [TASK_BATTERY_CURRENT] = DEFINE_TASK("BATTERY_CURRENT", NULL, NULL, batteryUpdateCurrentMeter, TASK_PERIOD_HZ(50), TASK_PRIORITY_MEDIUM), #ifdef USE_TRANSPONDER [TASK_TRANSPONDER] = DEFINE_TASK("TRANSPONDER", NULL, NULL, transponderUpdate, TASK_PERIOD_HZ(250), TASK_PRIORITY_LOW), #endif #ifdef USE_STACK_CHECK [TASK_STACK_CHECK] = DEFINE_TASK("STACKCHECK", NULL, NULL, taskStackCheck, TASK_PERIOD_HZ(10), TASK_PRIORITY_LOWEST), #endif [TASK_GYRO] = DEFINE_TASK("GYRO", NULL, NULL, taskGyroSample, TASK_GYROPID_DESIRED_PERIOD, TASK_PRIORITY_REALTIME), [TASK_FILTER] = DEFINE_TASK("FILTER", NULL, NULL, taskFiltering, TASK_GYROPID_DESIRED_PERIOD, TASK_PRIORITY_REALTIME), [TASK_PID] = DEFINE_TASK("PID", NULL, NULL, taskMainPidLoop, TASK_GYROPID_DESIRED_PERIOD, TASK_PRIORITY_REALTIME), #ifdef USE_ACC [TASK_ACCEL] = DEFINE_TASK("ACC", NULL, NULL, taskUpdateAccelerometer, TASK_PERIOD_HZ(1000), TASK_PRIORITY_MEDIUM), [TASK_ATTITUDE] = DEFINE_TASK("ATTITUDE", NULL, NULL, imuUpdateAttitude, TASK_PERIOD_HZ(100), TASK_PRIORITY_MEDIUM), #endif [TASK_RX] = DEFINE_TASK("RX", NULL, rxUpdateCheck, taskUpdateRxMain, TASK_PERIOD_HZ(33), TASK_PRIORITY_HIGH), // If event-based scheduling doesn't work, fallback to periodic scheduling [TASK_DISPATCH] = DEFINE_TASK("DISPATCH", NULL, NULL, dispatchProcess, TASK_PERIOD_HZ(1000), TASK_PRIORITY_HIGH), #ifdef USE_BEEPER [TASK_BEEPER] = DEFINE_TASK("BEEPER", NULL, NULL, beeperUpdate, TASK_PERIOD_HZ(100), TASK_PRIORITY_LOW), #endif #ifdef USE_GPS [TASK_GPS] = DEFINE_TASK("GPS", NULL, NULL, gpsUpdate, TASK_PERIOD_HZ(TASK_GPS_RATE), TASK_PRIORITY_MEDIUM), // Required to prevent buffer overruns if running at baud (115 bytes / period < 256 bytes buffer) #endif #ifdef USE_GPS_RESCUE [TASK_GPS_RESCUE] = DEFINE_TASK("GPS_RESCUE", NULL, NULL, taskGpsRescue, TASK_PERIOD_HZ(TASK_GPS_RESCUE_RATE_HZ), TASK_PRIORITY_MEDIUM), #endif #ifdef USE_MAG [TASK_COMPASS] = DEFINE_TASK("COMPASS", NULL, NULL, taskUpdateMag, TASK_PERIOD_HZ(TASK_COMPASS_RATE_HZ), TASK_PRIORITY_LOW), #endif #ifdef USE_BARO [TASK_BARO] = DEFINE_TASK("BARO", NULL, NULL, taskUpdateBaro, TASK_PERIOD_HZ(TASK_BARO_RATE_HZ), TASK_PRIORITY_LOW), #endif #if defined(USE_BARO) || defined(USE_GPS) [TASK_ALTITUDE] = DEFINE_TASK("ALTITUDE", NULL, NULL, taskCalculateAltitude, TASK_PERIOD_HZ(TASK_ALTITUDE_RATE_HZ), TASK_PRIORITY_LOW), #endif #ifdef USE_DASHBOARD [TASK_DASHBOARD] = DEFINE_TASK("DASHBOARD", NULL, NULL, dashboardUpdate, TASK_PERIOD_HZ(10), TASK_PRIORITY_LOW), #endif #ifdef USE_OSD [TASK_OSD] = DEFINE_TASK("OSD", NULL, osdUpdateCheck, osdUpdate, TASK_PERIOD_HZ(OSD_FRAMERATE_DEFAULT_HZ), TASK_PRIORITY_LOW), #endif #ifdef USE_TELEMETRY [TASK_TELEMETRY] = DEFINE_TASK("TELEMETRY", NULL, NULL, taskTelemetry, TASK_PERIOD_HZ(250), TASK_PRIORITY_LOW), #endif #ifdef USE_LED_STRIP [TASK_LEDSTRIP] = DEFINE_TASK("LEDSTRIP", NULL, NULL, ledStripUpdate, TASK_PERIOD_HZ(TASK_LEDSTRIP_RATE_HZ), TASK_PRIORITY_LOW), #endif #ifdef USE_BST [TASK_BST_MASTER_PROCESS] = DEFINE_TASK("BST_MASTER_PROCESS", NULL, NULL, taskBstMasterProcess, TASK_PERIOD_HZ(50), TASK_PRIORITY_LOWEST), #endif #ifdef USE_ESC_SENSOR [TASK_ESC_SENSOR] = DEFINE_TASK("ESC_SENSOR", NULL, NULL, escSensorProcess, TASK_PERIOD_HZ(100), TASK_PRIORITY_LOW), #endif #ifdef USE_CMS [TASK_CMS] = DEFINE_TASK("CMS", NULL, NULL, cmsHandler, TASK_PERIOD_HZ(20), TASK_PRIORITY_LOW), #endif #ifdef USE_VTX_CONTROL [TASK_VTXCTRL] = DEFINE_TASK("VTXCTRL", NULL, NULL, vtxUpdate, TASK_PERIOD_HZ(5), TASK_PRIORITY_LOWEST), #endif #ifdef USE_RCDEVICE [TASK_RCDEVICE] = DEFINE_TASK("RCDEVICE", NULL, NULL, rcdeviceUpdate, TASK_PERIOD_HZ(20), TASK_PRIORITY_MEDIUM), #endif #ifdef USE_CAMERA_CONTROL [TASK_CAMCTRL] = DEFINE_TASK("CAMCTRL", NULL, NULL, taskCameraControl, TASK_PERIOD_HZ(5), TASK_PRIORITY_LOW), #endif #ifdef USE_ADC_INTERNAL [TASK_ADC_INTERNAL] = DEFINE_TASK("ADCINTERNAL", NULL, NULL, adcInternalProcess, TASK_PERIOD_HZ(1), TASK_PRIORITY_LOWEST), #endif #ifdef USE_PINIOBOX [TASK_PINIOBOX] = DEFINE_TASK("PINIOBOX", NULL, NULL, pinioBoxUpdate, TASK_PERIOD_HZ(20), TASK_PRIORITY_LOWEST), #endif #ifdef USE_RANGEFINDER [TASK_RANGEFINDER] = DEFINE_TASK("RANGEFINDER", NULL, NULL, taskUpdateRangefinder, TASK_PERIOD_HZ(10), TASK_PRIORITY_LOWEST), #endif #ifdef USE_CRSF_V3 [TASK_SPEED_NEGOTIATION] = DEFINE_TASK("SPEED_NEGOTIATION", NULL, NULL, speedNegotiationProcess, TASK_PERIOD_HZ(100), TASK_PRIORITY_LOW), #endif #ifdef USE_RC_STATS [TASK_RC_STATS] = DEFINE_TASK("RC_STATS", NULL, NULL, rcStatsUpdate, TASK_PERIOD_HZ(100), TASK_PRIORITY_LOW), #endif };
上面就是使用DEFINE_TASK()宏定义创建了运行的各种任务,DEFINE_TASK()宏定义如下:
#define DEFINE_TASK(taskNameParam, subTaskNameParam, checkFuncParam, taskFuncParam, desiredPeriodParam, staticPriorityParam) {
\ .taskName = taskNameParam, \ .subTaskName = subTaskNameParam, \ .checkFunc = checkFuncParam, \ .taskFunc = taskFuncParam, \ .desiredPeriodUs = desiredPeriodParam, \ .staticPriority = staticPriorityParam \ }
可以在target.h通过宏定义配置需要执行的任务,TASK_COUNT是创建的任务数量,它不需要手动填入,它定义在一个任务枚举的最后,当前面的任务被宏定义开启越多,TASK_COUNT就会相应的增加。
#ifdef USE_PINIOBOX TASK_PINIOBOX, #endif #ifdef USE_CRSF_V3 TASK_SPEED_NEGOTIATION, #endif #ifdef USE_RC_STATS TASK_RC_STATS, #endif /* Count of real tasks */ TASK_COUNT, //创建的任务数量 /* Service task IDs */ TASK_NONE = TASK_COUNT, TASK_SELF } taskId_e;
2.3. 任务调度
任务创建完之后,就开始执行任务调度,它是一个BetaFlight基于bare-metal自研的一个时间片任务调度器。它类似一个具有优先级执行的时间片任务调度器,给各个任务分配一定的时间片,然后再根据优先级选择执行的任务。
scheduler ├──> <gyroEnabled> │ ├──> [Realtime gyro/filtering/PID tasks get complete priority] │ │ ├──> task_t *gyroTask = getTask(TASK_GYRO) │ │ ├──> nowCycles = getCycleCounter() │ │ ├──> nextTargetCycles = lastTargetCycles + desiredPeriodCycles │ │ └──> schedLoopRemainingCycles = cmpTimeCycles(nextTargetCycles, nowCycles) │ │ │ │ # Rootcause: USB VCP Data Transfer │ │ │ ├──> <schedLoopRemainingCycles < -desiredPeriodCycles> //错过了一个desiredPeriodCycles周期,导致剩余时间负值(且大于一个运行周期) │ │ ├──> nextTargetCycles += desiredPeriodCycles * (1 + (schedLoopRemainingCycles / -desiredPeriodCycles)) //常见USB配置工具连接的时候,尽力挽救scheduler算法 │ │ └──> schedLoopRemainingCycles = cmpTimeCycles(nextTargetCycles, nowCycles) │ │ │ │ # schedLoopStartMinCycles range rebound │ │ │ ├──> <(schedLoopRemainingCycles < schedLoopStartMinCycles) && (schedLoopStartCycles < schedLoopStartMaxCycles)> │ │ └──> schedLoopStartCycles += schedLoopStartDeltaUpCycles │ └──> <schedLoopRemainingCycles < schedLoopStartCycles> │ ├──> <schedLoopStartCycles > schedLoopStartMinCycles> │ │ └──> schedLoopStartCycles -= schedLoopStartDeltaDownCycles │ │ │ │ #轮询的方式读取陀螺仪数据 │ │ │ ├──> <while (schedLoopRemainingCycles > 0)> │ │ ├──> nowCycles = getCycleCounter(); │ │ └──> schedLoopRemainingCycles = cmpTimeCycles(nextTargetCycles, nowCycles); │ │ │ │ # 执行陀螺仪/滤波器/pid任务并检查rc链路中的数据可用性 │ │ │ ├──> currentTimeUs = micros() │ ├──> taskExecutionTimeUs += schedulerExecuteTask(gyroTask, currentTimeUs) │ ├──> <gyroFilterReady()> taskExecutionTimeUs += schedulerExecuteTask(getTask(TASK_FILTER), currentTimeUs) │ ├──> <pidLoopReady()> taskExecutionTimeUs += schedulerExecuteTask(getTask(TASK_PID), currentTimeUs) │ ├──> rxFrameCheck(currentTimeUs, cmpTimeUs(currentTimeUs, getTask(TASK_RX)->lastExecutedAtUs)) //检查是否有新的RC控制链路数据包收到 │ ├──> <cmp32(millis(), lastFailsafeCheckMs) > PERIOD_RXDATA_FAILURE> //核查、更新failsafe状态 │ ├──> lastTargetCycles = nextTargetCycles │ ├──> gyroDev_t *gyro = gyroActiveDev() //获取活跃的gyro设备 │ │ │ │ # 使用gyro_RATE_COUNT/gyro_LOCK_COUNT实现精确的陀螺仪时间同步 │ │ │ └──> [Sync scheduler into lock with gyro] // gyro->gyroModeSPI != GYRO_EXTI_NO_INT, 这里指数级收敛,只要desiredPeriodCycles越精准,理论精度会越高 │ ├──> [Calculate desiredPeriodCycles = sampleCycles / GYRO_RATE_COUNT and reset terminalGyroRateCount += GYRO_RATE_COUNT] │ └──> [Sync lastTargetCycles using exponential normalization by GYRO_RATE_COUNT/GYRO_LOCK_COUNT times iteration] ├──> nowCycles = getCycleCounter();schedLoopRemainingCycles = cmpTimeCycles(nextTargetCycles, nowCycles); ├──> <!gyroEnabled || (schedLoopRemainingCycles > (int32_t)clockMicrosToCycles(CHECK_GUARD_MARGIN_US))> │ │ │ │ # Find task need to be execute when there is time. │ │ │ ├──> currentTimeUs = micros() │ ├──> for (task_t *task = queueFirst(); task != NULL; task = queueNext()) <task->attribute->staticPriority != TASK_PRIORITY_REALTIME> //Update task dynamic priorities │ │ ├──> <task->attribute->checkFunc> //有属性检查函数 │ │ │ ├──> <task->dynamicPriority > 0> │ │ │ │ ├──> task->taskAgePeriods = 1 + (cmpTimeUs(currentTimeUs, task->lastSignaledAtUs) / task->attribute->desiredPeriodUs); │ │ │ │ └──> task->dynamicPriority = 1 + task->attribute->staticPriority * task->taskAgePeriods; │ │ │ ├──> <task->attribute->checkFunc(currentTimeUs, cmpTimeUs(currentTimeUs, task->lastExecutedAtUs))> │ │ │ │ ├──> const uint32_t checkFuncExecutionTimeUs = cmpTimeUs(micros(), currentTimeUs); │ │ │ │ ├──> checkFuncMovingSumExecutionTimeUs += checkFuncExecutionTimeUs - checkFuncMovingSumExecutionTimeUs / TASK_STATS_MOVING_SUM_COUNT; │ │ │ │ ├──> checkFuncMovingSumDeltaTimeUs += task->taskLatestDeltaTimeUs - checkFuncMovingSumDeltaTimeUs / TASK_STATS_MOVING_SUM_COUNT; │ │ │ │ ├──> checkFuncTotalExecutionTimeUs += checkFuncExecutionTimeUs; // time consumed by scheduler + task │ │ │ │ ├──> checkFuncMaxExecutionTimeUs = MAX(checkFuncMaxExecutionTimeUs, checkFuncExecutionTimeUs) │ │ │ │ ├──> task->lastSignaledAtUs = currentTimeUs │ │ │ │ ├──> task->taskAgePeriods = 1 │ │ │ │ └──> task->dynamicPriority = 1 + task->attribute->staticPriority │ │ │ └──> <else> │ │ │ └──> task->taskAgePeriods = 0 │ │ ├──> <!task->attribute->checkFunc> //无属性检查函数 │ │ │ ├──> task->taskAgePeriods = (cmpTimeUs(currentTimeUs, task->lastExecutedAtUs) / task->attribute->desiredPeriodUs) │ │ │ └──> <task->taskAgePeriods > 0> │ │ │ └──> task->dynamicPriority = 1 + task->attribute->staticPriority * task->taskAgePeriods │ │ └──> <task->dynamicPriority > selectedTaskDynamicPriority> │ │ ├──> timeDelta_t taskRequiredTimeUs = task->anticipatedExecutionTime >> TASK_EXEC_TIME_SHIFT │ │ ├──> int32_t taskRequiredTimeCycles = (int32_t)clockMicrosToCycles((uint32_t)taskRequiredTimeUs) │ │ ├──> taskRequiredTimeCycles += checkCycles + taskGuardCycles //增加守护时间(预留一些) │ │ └──> <taskRequiredTimeCycles < schedLoopRemainingCycles> || //剩余时间足够执行任务 │ │ <(scheduleCount & SCHED_TASK_DEFER_MASK) == 0> || │ │ <(task - tasks) == TASK_SERIAL)> //串行任务不阻塞 │ │ ├──> selectedTaskDynamicPriority = task->dynamicPriority; │ │ └──> selectedTask = task; //选中任务 │ │ │ │ # 选择任务执行 │ │ │ ├──> checkCycles = cmpTimeCycles(getCycleCounter(), nowCycles) //优先级调整,以及checkFunc运行需要计算耗时时间 │ └──> <selectedTask> │ ├──> timeDelta_t taskRequiredTimeUs = selectedTask->anticipatedExecutionTime >> TASK_EXEC_TIME_SHIFT // Recheck the available time as checkCycles is only approximate │ ├──> int32_t taskRequiredTimeCycles = (int32_t)clockMicrosToCycles((uint32_t)taskRequiredTimeUs) │ ├──> nowCycles = getCycleCounter() │ ├──> schedLoopRemainingCycles = cmpTimeCycles(nextTargetCycles, nowCycles) │ ├──> <!gyroEnabled || (taskRequiredTimeCycles < schedLoopRemainingCycles)> │ │ ├──> uint32_t antipatedEndCycles = nowCycles + taskRequiredTimeCycles; │ │ ├──> taskExecutionTimeUs += schedulerExecuteTask(selectedTask, currentTimeUs); │ │ ├──> nowCycles = getCycleCounter(); │ │ ├──> int32_t cyclesOverdue = cmpTimeCycles(nowCycles, antipatedEndCycles); │ │ ├──> <(currentTask - tasks) == TASK_RX> │ │ │ └──> skippedRxAttempts = 0 │ │ ├──> <(currentTask - tasks) == TASK_OSD> │ │ │ └──> skippedOSDAttempts = 0 │ │ ├──> <(cyclesOverdue > 0) || (-cyclesOverdue < taskGuardMinCycles)> //超时,但可控在taskGuardMinCycles范围之内 │ │ │ └──> <taskGuardCycles < taskGuardMaxCycles> │ │ │ └──> taskGuardCycles += taskGuardDeltaUpCycles //增加守护时间(预留更多一些) │ │ └──> <else if (taskGuardCycles > taskGuardMinCycles> //未超时 │ │ └──> taskGuardCycles -= taskGuardDeltaDownCycles; //减少守护时间 │ │ │ │ # 特殊任务处理 │ │ │ └──> <else <selectedTask->taskAgePeriods > TASK_AGE_EXPEDITE_COUNT>) || │ <((selectedTask - tasks) == TASK_OSD) && (TASK_AGE_EXPEDITE_OSD != 0) && (++skippedOSDAttempts > TASK_AGE_EXPEDITE_OSD)> || │ <((selectedTask - tasks) == TASK_RX) && (TASK_AGE_EXPEDITE_RX != 0) && (++skippedRxAttempts > TASK_AGE_EXPEDITE_RX)> │ └──> selectedTask->anticipatedExecutionTime *= TASK_AGE_EXPEDITE_SCALE └──> scheduleCount++;
任务调度是整个代码的核心,作为飞控自研的调度器,其有以下特点:
- 业务强耦合:鉴于IMU和PID业务对于飞行器飞行来说至关重要,因此该调度器在Gyro/Acc/PID的任务调用采用了精准时刻调度(+/-0.1us)。
- 任务优先级:MCU性能和业务逻辑复杂度,在至关重要任务调度之后剩余的时间片可能存在资源不够导致任务“饿死”得不到调度的情况。
- 边缘弹性任务时间优化:有限资源 + 非充分测试 + 资源可体感设计(OSD/CPU占用率等)
3.如何使用这个程序
3.1.目标文件源码分析
#pragma once #define TARGET_BOARD_IDENTIFIER "S411" #define USBD_PRODUCT_STRING "Betaflight STM32F411" #define USE_I2C_DEVICE_1 #define USE_I2C_DEVICE_2 #define USE_I2C_DEVICE_3 #define USE_UART1 #define USE_UART2 #define USE_UART6 #define SERIAL_PORT_COUNT (UNIFIED_SERIAL_PORT_COUNT + 3) #define USE_INVERTER #define USE_SPI_DEVICE_1 #define USE_SPI_DEVICE_2 #define USE_SPI_DEVICE_3 #define TARGET_IO_PORTA 0xffff #define TARGET_IO_PORTB 0xffff #define TARGET_IO_PORTC 0xffff #define TARGET_IO_PORTD 0xffff #define TARGET_IO_PORTE 0xffff #define USE_I2C #define I2C_FULL_RECONFIGURABILITY #define USE_DSHOT_BITBAND #define USE_BEEPER #ifdef USE_SDCARD #define USE_SDCARD_SPI #define USE_SDCARD_SDIO #endif #define USE_SPI #define SPI_FULL_RECONFIGURABILITY #define USE_VCP #define USE_SOFTSERIAL1 #define USE_SOFTSERIAL2 #define UNIFIED_SERIAL_PORT_COUNT 3 #define USE_USB_DETECT #define USE_ESCSERIAL #define USE_ADC #define USE_CUSTOM_DEFAULTS
基本模块是飞行器必须要有的,缺少就飞不起来;拓展模块有些是根据自己实际需要添加。驱动这些模块本质还是配置单片机的功能外设。
3.2.添加目标文件
#pragma once #define TARGET_BOARD_IDENTIFIER "STM32F411_MyFirmware" #define USBD_PRODUCT_STRING "STM32F411_MyFirmware" /* ======== LED ======== */ #define USE_LED_STRIP #define USE_LED_STRIP_STATUS_MODE #define LED0_PIN PC14 /* ======== UART ======== */ #define USE_UART #define USE_VCP #define USE_UART1 #define UART1_RX_PIN PA10 #define UART1_TX_PIN PA9 #define USE_UART2 #define UART2_RX_PIN PA3 #define UART2_TX_PIN PA2 #define SERIAL_PORT_COUNT 3 /* ======== SPI ======== */ #define USE_SPI #define USE_SPI_DEVICE_1 #define SPI1_SCK_PIN PA5 #define SPI1_MISO_PIN PA6 #define SPI1_MOSI_PIN PA7 #define SPI1_NSS_PIN PA4 #define USE_SPI_DEVICE_2 #define SPI2_SCK_PIN PB13 #define SPI2_MISO_PIN PB14 #define SPI2_MOSI_PIN PB15 #define USE_SPI_DEVICE_3 #define SPI3_SCK_PIN PB3 #define SPI3_MISO_PIN PB4 #define SPI3_MOSI_PIN PB5 /* ======== GYRO & ACC ======== */ #define USE_ACC #define USE_GYRO #define USE_SPI_GYRO #define USE_ACCGYRO_BMI270 #define USE_EXTI #define USE_GYRO_EXTI #define GYRO_1_EXTI_PIN PB6 #define USE_MPU_DATA_READY_SIGNAL #define GYRO_1_CS_PIN SPI1_NSS_PIN #define GYRO_1_SPI_INSTANCE SPI1 #define GYRO_1_ALIGN CW180_DEG /* ======== OSD ======== */ #define USE_OSD #define USE_CANVAS #define USE_CMS #define USE_CMS_FAILSAFE_MENU #define USE_EXTENDED_CMS_MENUS #define USE_MSP_DISPLAYPORT #define USE_OSD_OVER_MSP_DISPLAYPORT #define USE_OSD_ADJUSTMENTS #define USE_OSD_PROFILES #define USE_OSD_STICK_OVERLAY #define USE_MAX7456 #define MAX7456_SPI_CS_PIN PB12 #define MAX7456_SPI_INSTANCE SPI2 /* ======== VTX ======== */ #define USE_VTX #define USE_VTX_COMMON #define USE_VTX_CONTROL #define USE_VTX_MSP #define USE_VTX_TABLE #define USE_VTX_RTC6705 #define SPI_SHARED_MAX7456_AND_RTC6705 #define RTC6705_CS_PIN PA14 #define RTC6705_SPI_INSTANCE SPI2 #define CMS_SKIP_EMPTY_VTX_TABLE_ENTRIES /* ======== RX ======== */ #define USE_RX_SPI #define USE_RX_PPM #define USE_RX_PWM #define USE_SERIALRX #define USE_SERIALRX_CRSF // Team Black Sheep Crossfire protocol #define USE_SERIALRX_GHST // ImmersionRC Ghost Protocol #define USE_SERIALRX_IBUS // FlySky and Turnigy receivers #define USE_SERIALRX_SBUS // Frsky and Futaba receivers #define USE_SERIALRX_SPEKTRUM // SRXL, DSM2 and DSMX protocol #define USE_SERIALRX_FPORT // FrSky FPort #define USE_SERIALRX_XBUS // JR #define USE_SERIALRX_SRXL2 // Spektrum SRXL2 protocol #define USE_SERIALRX_JETIEXBUS #define USE_SERIALRX_SUMD // Graupner Hott protocol #define USE_SERIALRX_SUMH // Graupner legacy protocol #define USE_CRSF_V3 #define USE_CRSF_CMS_TELEMETRY #define USE_CRSF_LINK_STATISTICS #define USE_TELEMETRY #define USE_TELEMETRY_FRSKY_HUB #define USE_TELEMETRY_SMARTPORT #define USE_TELEMETRY_CRSF #define USE_TELEMETRY_GHST #define USE_TELEMETRY_SRXL #define USE_TELEMETRY_IBUS #define USE_TELEMETRY_IBUS_EXTENDED #define USE_TELEMETRY_JETIEXBUS #define USE_TELEMETRY_MAVLINK #define USE_TELEMETRY_HOTT #define USE_TELEMETRY_LTM #define USE_SPEKTRUM_BIND #define USE_SPEKTRUM_BIND_PLUG #define USE_SPEKTRUM_REAL_RSSI #define USE_SPEKTRUM_FAKE_RSSI #define USE_SPEKTRUM_RSSI_PERCENT_CONVERSION #define USE_SPEKTRUM_VTX_CONTROL #define USE_SPEKTRUM_VTX_TELEMETRY #define USE_SPEKTRUM_CMS_TELEMETRY #define RX_SPI_INSTANCE SPI3 #define RX_SPI_LED_INVERTED #define RX_NSS_PIN PA15 #define RX_SPI_LED_PIN PC15 #define RX_SPI_EXTI_PIN PC13 #define RX_SPI_BIND_PIN PB2 #define RX_EXPRESSLRS_SPI_RESET_PIN PB9 #define RX_EXPRESSLRS_SPI_BUSY_PIN PA13 #define RX_EXPRESSLRS_TIMER_INSTANCE TIM5 #define USE_TELEMETRY #define USE_RX_EXPRESSLRS #define USE_RX_SX1280 #define DEFAULT_RX_FEATURE FEATURE_RX_SPI #define RX_SPI_DEFAULT_PROTOCOL RX_SPI_EXPRESSLRS /* ======== ADC ======== */ #define USE_ADC #define ADC_INSTANCE ADC1 #define ADC1_DMA_OPT 0 #define VBAT_ADC_PIN PA1 #define CURRENT_METER_ADC_PIN PB0 #define VBAT_SCALE_DEFAULT 110 #define CURRENT_METER_SCALE_DEFAULT 510 #define CURRENT_METER_OFFSET_DEFAULT 0 #define DEFAULT_VOLTAGE_METER_SOURCE VOLTAGE_METER_ADC #define DEFAULT_CURRENT_METER_SOURCE CURRENT_METER_ADC /* ======== ESC ======== */ #define USE_DSHOT #define USE_DSHOT_DMAR #define USE_DSHOT_BITBANG #define USE_DSHOT_TELEMETRY #define USE_DSHOT_TELEMETRY_STATS #define USE_BRUSHED_ESC_AUTODETECT // Detect if brushed motors are connected and set defaults appropriately to avoid motors spinning on boot #define ENABLE_DSHOT_DMAR DSHOT_DMAR_ON #define DSHOT_BITBANG_DEFAULT DSHOT_BITBANG_AUTO /* ======== OTHER ======== */ #define USE_BLACKBOX #define USE_SERVOS #define USE_PINIO #define USE_PINIOBOX #define DEFAULT_FEATURES (FEATURE_INFLIGHT_ACC_CAL | FEATURE_LED_STRIP | FEATURE_OSD) #define TARGET_IO_PORTA 0xffff #define TARGET_IO_PORTB 0xffff #define TARGET_IO_PORTC 0xffff #define TARGET_IO_PORTD (BIT(2)) #define USE_BEEPER #define USABLE_TIMER_CHANNEL_COUNT 5 #define USED_TIMERS ( TIM_N(2) | TIM_N(3) | TIM_N(4) ) #define USE_TARGET_CONFIG
target.c里主要是配置电机、WS2812、蜂鸣器等需要PWM的驱动。
#include <stdint.h> #include "platform.h" #include "drivers/io.h" #include "drivers/dma.h" #include "drivers/timer.h" #include "drivers/timer_def.h" const timerHardware_t timerHardware[USABLE_TIMER_CHANNEL_COUNT] = {
DEF_TIM(TIM4, CH3, PB8, TIM_USE_MOTOR, 0, 0), // M1 DEF_TIM(TIM2, CH1, PA0, TIM_USE_MOTOR, 0, 0), // M2 DEF_TIM(TIM2, CH3, PB10, TIM_USE_MOTOR, 0, 0), // M3 DEF_TIM(TIM4, CH2, PB7, TIM_USE_MOTOR, 0, 0), // M4 DEF_TIM(TIM3, CH4, PB1, TIM_USE_LED, 0, 0) // LED Strip };
其实target的配置,就是相当于在betaflight configurator的CLI窗口,一行一行的输入,然后把数据保存到飞控芯片的flash。
3.3.编译程序
make arm_sdk_install
系统就会自动安装本工程需要的gcc-arm编译器,安装路径就在betaflight/downloads文件夹下,这样的好处就是移植方便,把代码放到另一台还没安装gcc-arm编译器的电脑上也能编译。
make target名称
看到上面的提示代表成功生成固件了,文件路径就在betaflight/obj/main目录下
4.参考文献
1.BetaFlight开源工程结构简明介绍
2.BetaFlight开源代码框架简介
3.betaflight 代码结构
4.betaflight官方使用说明
免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://haidsoft.com/109994.html

