Keil MDK STM32系列(十) Ubuntu下的PlatformIO开发环境
Keil MDK STM32系列
- Keil MDK STM32系列(一) 基于标准外设库SPL的STM32F103开发
- Keil MDK STM32系列(二) 基于标准外设库SPL的STM32F401开发
- Keil MDK STM32系列(三) 基于标准外设库SPL的STM32F407开发
- Keil MDK STM32系列(四) 基于抽象外设库HAL的STM32F401开发
- Keil MDK STM32系列(五) 使用STM32CubeMX创建项目基础结构
- Keil MDK STM32系列(六) 基于HAL的ADC模数转换
- Keil MDK STM32系列(七) 基于HAL的PWM和定时器
- Keil MDK STM32系列(八) 基于HAL的PWM和定时器输出音频
- Keil MDK STM32系列(九) 基于HAL和FatFs的FAT格式SD卡TF卡读写
- Keil MDK STM32系列(十) Ubuntu下的PlatformIO开发环境
这篇和Keil MDK没什么关系, 但是HAL库和开发方式是一样的, 也放在这个系列下吧
PlatformIO
PlatformIO是VSCode的一个扩展, 主要面向的是嵌入式的开发, 因为VSCode的跨平台属性, PlatformIO也是跨平台的. 这里主要介绍在Ubuntu20.04下的PlatformIO环境.
安装, 略
PlatformIO下的STM32烧录工具
对于STM32, PlatformIO支持的烧录工具有 blackmagic, cmsis-dap, dfu, jlink, serial, stlink, 直接选stlink就可以了.
PlatformIO下的STM32封装库
在PlatformIO下, 在Platforms中安装ST STM32, 这个Platform中包含了多个适用于STM32开发的framework
Libopencm3
没找到安装路径... 这是一套面向ARM Cortex-M架构微处理器的开源固件库, 支持STM32, Atmel SAM3x, NXP LPC, TI LM4F等系列的MCU, 对这个不熟就不介绍了
Arduino
依托于Arduino的库环境, 使用Arduino的封装接口操作STM32, 适合初学者
package的路径在 [user home]/.platformio/packages/framework-arduinoststm32 , 大小接近500MB
Cmsis
这部分类似于Keil MDK下的SPL库, 包含多个package
- cmsis核心库的安装路径是 /home/[your user]/.platformio/packages/framework-cmsis, 大小103MB
- 对应了STM32Cube完整库 Drivers/CMSIS/ 路径下的内容
- 多了一个Driver目录
- stm32f4-framework库文件安装路径是 /home/[your user]/.platformio/packages/framework-cmsis-stm32f4, 大小30MB
- 这个库只是
STM32Cube MCU Full Package
的核心定义部分, 在Github上的仓库地址: https://github.com/STMicroelectronics/cmsis_device_f4 - 这个库有自己的版本号v2.6.x, 容易与完整库的版本号混淆, 其与
CMSIS Core
和Full MCU package
的对应关系在README.md的表格中, 现在最新的是v2.6.7, 对应的完整库版本为v1.26.2 - 这个库与Windows Keil5 MDK下使用的标准外设库
Standard peripherals library
不兼容, 后者已经不再更新, 最高版本到1.8.0
- 这个库只是
- STM32Cube完整库的仓库
- Github地址是 https://github.com/STMicroelectronics/STM32CubeF4
- 上面的framework库, 在这个仓库里的路径是 tree/master/Drivers/CMSIS/Device/ST/STM32F4xx, 可以对比两边的文件, 是一样的
- 完整库比较大, 压缩包接近300MB
综上, STM32F4的CMSIS环境下是没有可用的标准外设库(Standard peripherals library)的, 尝试过用1.8.0的标准外设库, 编译有不少错误, 所以用这个开发的话
- 要么自己修订标准外设库, 使其兼容v2.6.x的CMSIS核心库, 这个需要对各外设的变量和结构体很熟, 我这样刚入门的估计是搞不定
- 要么不用标准外设库, 纯使用核心库变量开发. 这个难度也不小, 类似于回到8051开发的模式了, 每写一步都要查寄存器手册.
- 要么直接用STM32Cube的完整外设库. 不过既然都用了STM32Cube的库了, 为什么不直接选framework时就选STM32Cube呢?
Stm32cube
文档: https://docs.platformio.org/en/latest/frameworks/stm32cube.html
和Windows下的STM32CubeMX使用了相同的HAL库, package路径 [user home]/.platformio/packages/framework-stm32cubef4 , 大小 78.3MB
这个package下的内容, 就是STM32CubeF4仓库的内容, 当前版本是1.26.2.
使用STM32CubeF4开发的项目, 用这个Framework是可以直接编译的.
STM32CubeF4 的文件结构
这个库有三个目录
STM32CubeF4/Drivers
这里包含了核心库 CMSIS, HAL库 STM32F4xx_HAL_Driver, 还有众多的外设支持, 简直是个宝库
├── Drivers
│ ├── BSP
│ │ ├── Adafruit_Shield
│ │ ├── Components
│ │ │ ├── ampire480272
│ │ │ ├── ampire640480
│ │ │ ├── Common
│ │ │ ├── cs43l22
│ │ │ ├── exc7200
│ │ │ ├── ft6x06
│ │ │ ├── i3g4250d
│ │ │ ├── ili9325
│ │ │ ├── ili9341
│ │ │ ├── l3gd20
│ │ │ ├── lis302dl
│ │ │ ├── lis3dsh
│ │ │ ├── ls016b8uy
│ │ │ ├── lsm303agr
│ │ │ ├── lsm303dlhc
│ │ │ ├── mfxstm32l152
│ │ │ ├── n25q128a
│ │ │ ├── n25q256a
│ │ │ ├── n25q512a
│ │ │ ├── nt35510
│ │ │ ├── otm8009a
│ │ │ ├── ov2640
│ │ │ ├── ov5640
│ │ │ ├── s25fl512s
│ │ │ ├── s5k5cag
│ │ │ ├── st7735
│ │ │ ├── st7789h2
│ │ │ ├── stmpe1600
│ │ │ ├── stmpe811
│ │ │ ├── ts3510
│ │ │ └── wm8994
│ │ ├── STM32412G-Discovery
│ │ ├── STM32446E_EVAL
│ │ ├── STM32469I-Discovery
│ │ ├── STM32469I_EVAL
│ │ ├── STM324x9I_EVAL
│ │ ├── STM324xG_EVAL
│ │ ├── STM32F401-Discovery
│ │ ├── STM32F411E-Discovery
│ │ ├── STM32F413H-Discovery
│ │ ├── STM32F429I-Discovery
│ │ ├── STM32F4-Discovery
│ │ ├── STM32F4xx-Nucleo
│ │ └── STM32F4xx_Nucleo_144
│ ├── CMSIS
│ │ ├── Core
│ │ ├── Core_A
│ │ ├── Device
│ │ ├── DSP
│ │ ├── Include
│ │ ├── Lib
│ │ │ └── GCC
│ │ ├── NN
│ │ ├── RTOS
│ │ └── RTOS2
│ └── STM32F4xx_HAL_Driver
│ ├── Inc
│ └── Src
STM32CubeF4/Middlewares
这个目录下是USB功能封装和一些常用的第三方库, 例如FatFS, LwIP等
├── Middlewares
│ ├── ST
│ │ ├── STemWin
│ │ ├── STM32_Audio
│ │ │ └── Addons
│ │ │ └── PDM
│ │ ├── STM32_USB_Device_Library
│ │ ├── STM32_USB_Host_Library
│ │ └── TouchGFX
│ └── Third_Party
│ ├── FatFS
│ ├── FreeRTOS
│ ├── LibJPEG
│ ├── LwIP
│ └── mbedTLS
STM32CubeF4/Utilities
这里是一些工具和字体
└── Utilities
├── CPU
├── Fonts
├── Log
└── Media
├── Audio
├── Pictures
└── Video
使用Stm32cube创建项目
打开PlatformIO:Home, 如果没装 Platform:ST STM32 的, 先在 Platform 里装一下
- 点击 New Project,
- 填入项目名称,
- 在 Board 中输入 stm32f401 在过滤结果的列表中, 选择 stm32f401cc, 如果是常见的 WeAct Studio BlackPill, 可以直接在底下找到对应的专门的board
- 选择 Framework 为 STM32Cube
- 如果不希望建在默认目录的话, 取消 Use Default Location 勾选, 指定位置, PlatformIO 会创建项目目录
platformio.ini
这个文件的内容如下, 一个项目里可以有多个env, 可以指定一个默认的, 在使用快捷键时, 会执行默认env的编译和写入.
[env:blackpill_f401cc]
platform = ststm32
board = blackpill_f401cc
framework = stm32cube
;通用的 stm32f401cc 配置
[env:stm32f401cc]
platform = ststm32
board = genericSTM32F401CC
framework = stm32cube
upload_protocol = stlink
board_build.stm32cube.custom_config_header = yes
参数说明:
- upload_protocol 用于指定不同的写入方式
- board_build.stm32cube.custom_config_header = yes 禁止platformio自动生成
stm32f4xx_hal_conf.h
. 默认情况下, 在项目启动或编译时, platformio会自动在库文件目录, 基于stm32f4xx_hal_conf_template.h
复制创建stm32f4xx_hal_conf.h
, 而HAL项目中这个配置文件一般是和main.c一起放在用户代码目录下的, 如果按默认的方式, 编译时会优先使用库目录下的文件, 用户自己配置的就失效了. 加上这个选项可以禁止platformio自己生成这个文件.
.vscode/c_cpp_properties.json
这个文件很重要, 因为任何 include 的错误, 都可以在这里检查. 这个文件是 PlatformIO 自动生成的, 所以不需要去改它, 如果发现你修改了platformio.ini 后它没有更新, 重新打开 VS Code 就可以了.
stm32f4xx_hal_conf.h
从 [user home]/.platformio/packages/framework-stm32cubef4/Drivers/STM32F4xx_HAL_Driver/Inc 中复制 stm32f4xx_hal_conf_template.h 到 src 目录, 并重命名为 stm32f4xx_hal_conf.h
需要修改的两处
- 模块启用部分. define xxxx ENABLED 这部分只需要保留下面的这些, 其它全部注释掉, 只有用到的才需要取消注释
#define HAL_MODULE_ENABLED
#define HAL_GPIO_MODULE_ENABLED
#define HAL_EXTI_MODULE_ENABLED
#define HAL_DMA_MODULE_ENABLED
#define HAL_RCC_MODULE_ENABLED
#define HAL_FLASH_MODULE_ENABLED
#define HAL_PWR_MODULE_ENABLED
#define HAL_CORTEX_MODULE_ENABLED
- 设置外部晶振频率, 设置成板载晶振的频率
#define HSE_VALUE 8000000U /*!< Value of the External oscillator in Hz */
项目文件
以下文件都直接在 src 目录下创建, 演示A6, A7两个PIN连接的LED间隔2秒闪灯
main.c
#include "main.h"
void SystemClock_Config(void);
void SystemClock_Config_HSE_84MHz(void);
static void MX_GPIO_Init(void);
int main(void)
{
HAL_Init();
SystemClock_Config_HSE_84MHz();
MX_GPIO_Init();
while (1)
{
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_6 | GPIO_PIN_7, GPIO_PIN_SET);
HAL_Delay(2000);
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_6 | GPIO_PIN_7, GPIO_PIN_RESET);
HAL_Delay(2000);
}
}
void SystemClock_Config(void)
{
RCC_OscInitTypeDef RCC_OscInitStruct = {0};
RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
/** Configure the main internal regulator output voltage
*/
__HAL_RCC_PWR_CLK_ENABLE();
__HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_VOLTAGE_SCALE1);
/** Initializes the RCC Oscillators according to the specified parameters
* in the RCC_OscInitTypeDef structure.
*/
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
RCC_OscInitStruct.HSEState = RCC_HSE_ON;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
RCC_OscInitStruct.PLL.PLLM = 4;
RCC_OscInitStruct.PLL.PLLN = 84;
RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV2;
RCC_OscInitStruct.PLL.PLLQ = 4;
if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
{
Error_Handler();
}
/** Initializes the CPU, AHB and APB buses clocks
*/
RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
|RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2;
RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;
if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK)
{
Error_Handler();
}
}
void SystemClock_Config_HSE_84MHz(void)
{
RCC_OscInitTypeDef RCC_OscInitStruct = {0};
RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
__HAL_RCC_PWR_CLK_ENABLE();
__HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_VOLTAGE_SCALE2);
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
RCC_OscInitStruct.HSEState = RCC_HSE_ON;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
RCC_OscInitStruct.PLL.PLLM = 25;
RCC_OscInitStruct.PLL.PLLN = 168;
RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV2;
RCC_OscInitStruct.PLL.PLLQ = 4;
if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
{
Error_Handler();
}
RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK|RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2;
RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;
if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK)
{
Error_Handler();
}
}
static void MX_GPIO_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
/* GPIO Ports Clock Enable */
__HAL_RCC_GPIOH_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();
/*Configure GPIO pin Output Level */
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_6|GPIO_PIN_7, GPIO_PIN_RESET);
/*Configure GPIO pins : PA6 PA7 */
GPIO_InitStruct.Pin = GPIO_PIN_6|GPIO_PIN_7;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
}
void Error_Handler(void)
{
/* USER CODE BEGIN Error_Handler_Debug */
/* User can add his own implementation to report the HAL error return state */
__disable_irq();
while (1)
{
}
/* USER CODE END Error_Handler_Debug */
}
#ifdef USE_FULL_ASSERT
void assert_failed(uint8_t *file, uint32_t line)
{
/* USER CODE BEGIN 6 */
/* User can add his own implementation to report the file name and line number,
ex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) */
/* USER CODE END 6 */
}
#endif /* USE_FULL_ASSERT */
main.h
#ifndef __MAIN_H
#define __MAIN_H
#ifdef __cplusplus
extern "C" {
#endif
/* Includes ------------------------------------------------------------------*/
#include "stm32f4xx_hal.h"
void Error_Handler(void);
#ifdef __cplusplus
}
#endif
#endif /* __MAIN_H */
stm32f4xx_hal_msp.c
#include "stm32f4xx_hal.h"
void HAL_MspInit(void)
{
__HAL_RCC_SYSCFG_CLK_ENABLE();
__HAL_RCC_PWR_CLK_ENABLE();
}
stm32f4xx_it.c
#include "stm32f4xx_hal.h"
#include "stm32f4xx_it.h"
/******************************************************************************/
/* Cortex-M4 Processor Interruption and Exception Handlers */
/******************************************************************************/
void NMI_Handler(void)
{
while (1)
{
}
}
void HardFault_Handler(void)
{
while (1)
{
}
}
void MemManage_Handler(void)
{
while (1)
{
}
}
void BusFault_Handler(void)
{
while (1)
{
}
}
void UsageFault_Handler(void)
{
while (1)
{
}
}
void SVC_Handler(void)
{
}
void DebugMon_Handler(void)
{
}
void PendSV_Handler(void)
{
}
void SysTick_Handler(void)
{
HAL_IncTick();
}
stm32f4xx_it.h
#ifndef __STM32F4xx_IT_H
#define __STM32F4xx_IT_H
#ifdef __cplusplus
extern "C" {
#endif
/* Exported functions prototypes ---------------------------------------------*/
void NMI_Handler(void);
void HardFault_Handler(void);
void MemManage_Handler(void);
void BusFault_Handler(void);
void UsageFault_Handler(void);
void SVC_Handler(void);
void DebugMon_Handler(void);
void PendSV_Handler(void);
void SysTick_Handler(void);
#ifdef __cplusplus
}
#endif
#endif /* __STM32F4xx_IT_H */
STM32F401CCxx 可用的 SystemClock_Config()
配置不正确的系统时钟会导致系统卡死, 下面两个方法是测试可用的
/**
* Generated by STM32Cube 1.26.x
* *) Enable HSE(25MHz), No LSE(32.768KHz)
* *) No PLL, No prescaler, HSE->SYSCLK->PHBPrescaler=1->AHB,APB1,APB2...
*/
void SystemClock_Config_HSE_25MHz(void)
{
RCC_OscInitTypeDef RCC_OscInitStruct = {0};
RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
__HAL_RCC_PWR_CLK_ENABLE();
__HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_VOLTAGE_SCALE2);
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
RCC_OscInitStruct.HSEState = RCC_HSE_ON;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_NONE;
if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
{
Error_Handler();
}
RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK|RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_HSE;
RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV1;
//RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;
RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV2; // Make APB2 12.5MHz
if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_0) != HAL_OK)
{
Error_Handler();
}
}
/**
* Generated by STM32Cube 1.26.x
* *) Enable HSE(25MHz), No LSE(32.768KHz)
* *) Enable PLL
* *) HSE->PLL->/25->*168->/2->PPLCLK->SYSCLK(84MHz)->APB1 /2-> APB1
*/
void SystemClock_Config_HSE_84MHz(void)
{
RCC_OscInitTypeDef RCC_OscInitStruct = {0};
RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
__HAL_RCC_PWR_CLK_ENABLE();
__HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_VOLTAGE_SCALE2);
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
RCC_OscInitStruct.HSEState = RCC_HSE_ON;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
RCC_OscInitStruct.PLL.PLLM = 25;
RCC_OscInitStruct.PLL.PLLN = 168;
RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV2;
RCC_OscInitStruct.PLL.PLLQ = 4;
if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
{
Error_Handler();
}
RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK|RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2;
RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;
if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK)
{
Error_Handler();
}
}
用UART输出printf的宏定义
因为sdcc和keil5 mdk下的编译工具不一样, 所以这部分的定义也不一样, 可以用下面的宏语句统一处理
#if defined(__GNUC__)
int _write(int fd, char *ptr, int len)
{
HAL_UART_Transmit(&huart1, (uint8_t *)ptr, len, 0xFFFF);
return len;
}
#elif defined (__ICCARM__)
size_t __write(int handle, const unsigned char * buffer, size_t size)
{
HAL_UART_Transmit(&huart1, (uint8_t *) buffer, size, HAL_MAX_DELAY);
return size;
}
#elif defined (__CC_ARM)
int fputc(int ch, FILE *f)
{
HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, HAL_MAX_DELAY);
return ch;
}
#endif
或者
#ifdef __GNUC__
#define PUTCHAR_PROTOTYPE int __io_putchar(int ch)
#else
#define PUTCHAR_PROTOTYPE int fputc(int ch, FILE *f)
#endif /* __GNUC__ */
/**
* @brief Retargets the C library printf function to the USART.
* @param None
* @retval None
*/
PUTCHAR_PROTOTYPE
{
HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, HAL_MAX_DELAY);
return ch;
}
参考
- https://docs.platformio.org/en/latest/frameworks/stm32cube.html
- https://docs.platformio.org/en/latest/tutorials/ststm32/stm32cube_debugging_unit_testing.html#tutorial-stm32cube-debugging-unit-testing
- STM32 HAL库的介绍 https://blog.csdn.net/xuzhexing/article/details/90137754
- HAL库的nRF24L01操作 https://github.com/pstolarz/NRF_HAL/blob/master/src/nrf_hal.cpp
- 同上 https://github.com/leech001/NRF24L01/blob/master/src/nrf24l01.c
- 同上 https://github.com/eos1d3/NRF24L01
- 同上 https://github.com/J20RC/STM32_RC_Transmitter/tree/master/software/Config