- Java锁的逻辑(结合对象头和ObjectMonitor)
- 还在用饼状图?来瞧瞧这些炫酷的百分比可视化新图形(附代码实现)⛵
- 自动注册实体类到EntityFrameworkCore上下文,并适配ABP及ABPVNext
- 基于Sklearn机器学习代码实战
嵌入式常用的图形显示库 。
对设备的要求是 "all you need is at least 32kB RAM and 128 kB Flash, a C compiler, a frame buffer, and at least an 1/10 screen sized buffer for rendering". 最低要求是128KB Flash, 但实际上这个大小基本上什么也做不了, 所以直接用256K Flash 的 AIR32F103CCT6 和 AIR32F103RPT6. 。
Principle 大佬写过一篇 Air32F103试玩-移植LVGL+FreeRTOS Keil5 用户可以参考. 基于STM32标准库, 用的屏幕是 GC9306X 320x240LCD. 。
我没有这个型号的屏幕, 手里能找到现成的串口屏只有一个128x160的ST7735, 就用这个做测试吧. 。
参考LVGL的文档, 这两片内容差不多的, 第二篇会更细节一点 。
需要做的步骤为 。
从 https://github.com/lvgl/lvgl/releases 下载LVGL, 当前版本是v8.3.5, 解压. 。
在项目 Libraries 下创建lvgl目录, 复制必须的文件到这个目录下 。
demos/
examples/
src/
LICENCE.txt
lv_conf_template.h
lvgl.h
lvgl.mk
复制后的 Libraries 目录结构为 。
Libraries
├───AIR32F10xLib
├───CMSIS
├───Debug
├───DeviceSupport
├───EPaper
├───FreeRTOS
├───Helix
├───LDScripts
├───lvgl
│ ├───demos
│ ├───examples
│ └───src
│ ├───core
│ ├───draw
│ ├───extra
│ ├───font
│ ├───hal
│ ├───misc
│ └───widgets
在 Makefile 中添加 LVGL 选项 。
# Build with lvgl, y:yes, n:no
USE_LVGL ?= n
LVGL的编译列表和头文件路径都已经在 lvgl.mk 里定义好了, 这里只需要把它 include 进来, 再合并到项目的列表中. 。
ifeq ($(USE_LVGL),y)
LVGL_DIR ?= Libraries
LVGL_DIR_NAME ?= lvgl
include Libraries/lvgl/lvgl.mk
CFILES += $(CSRCS)
INCLUDES += Libraries/lvgl
else
CFLAGS ?=
endif
将 USE_LVGL 设为 y 之后, make 就会带上 LVGL 一起编译. 因为 LVGL 文件很多, 编译时间较长, 可以根据自己电脑的CPU个数设置并发编译, 例如对于8个逻辑核的L480, 可以执行 。
make -j8
因为编译结果有200多KByte, 写入的速度也很慢, 暂时没有什么好办法. 。
将 lvgl/lv_conf_template.h, 复制到 user 目录下, 改名为 lv_conf.h, 编辑 。
将 #if 0 改为 #if 1 。
/* clang-format off */
#if 1 /*Set it to "1" to enable content*/
因为ST7735支持的是2byte的像素, 色深设为 16 。
/*Color depth: 1 (1 byte per pixel), 8 (RGB332), 16 (RGB565), 32 (ARGB8888)*/
#define LV_COLOR_DEPTH 16
再往下, 都是用0和1代表对应功能项的关和开, 可以保持默认. 因为ST7735屏幕分辨率较小, 所以再修改一下字体, 将 LV_FONT_MONTSERRAT_10 改为1, 将 LV_FONT_MONTSERRAT_14 改为0 启用10像素字体 。
#define LV_FONT_MONTSERRAT_10 0
#define LV_FONT_MONTSERRAT_12 0
#define LV_FONT_MONTSERRAT_14 1
再设置一下 LV_FONT_DEFAULT , 改为 &lv_font_montserrat_10 , 替换为刚才启用的 10像素字体 。
/*Always set a default font*/
#define LV_FONT_DEFAULT &lv_font_montserrat_14
这里使用TIM3, 将定时间隔设为1毫秒, 开启 TIM_IT_Update 中断 。
void TIM3_Configuration(void)
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
NVIC_InitTypeDef NVIC_InitStructure;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
// Set counter limit to 100 -- interval will be 1ms
TIM_TimeBaseStructure.TIM_Period = 100 - 1;
/**
* Clock source of TIM2,3,4,5,6,7: if(APB1 prescaler =1) then PCLK1 x1, else PCLK1 x2
* */
if (clocks.HCLK_Frequency == clocks.PCLK1_Frequency)
{
// clock source is PCLK1 x1.
// Note: TIM_Prescaler is 16bit, [0, 65535], given PCLK1 is 36MHz, divider should > 550
TIM_TimeBaseStructure.TIM_Prescaler = clocks.PCLK1_Frequency / 100000 - 1;
}
else
{
// clock source is PCLK1 x2
TIM_TimeBaseStructure.TIM_Prescaler = clocks.PCLK1_Frequency * 2 / 100000 - 1;
}
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; // TDTS = Tck_tim
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure);
// Enable interrupt from 'TIM update'
TIM_ITConfig(TIM3, TIM_IT_Update, ENABLE);
// NVIC config
NVIC_InitStructure.NVIC_IRQChannel = TIM3_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
TIM_Cmd(TIM3, ENABLE);
}
创建TIM3的中断回调函数, 因为定时器间隔为1毫秒, 因此使用 lv_tick_inc(1) 。
void TIM3_IRQHandler(void)
{
if (TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET)
{
// Clear INT flag
TIM_ClearITPendingBit(TIM3, TIM_IT_Update);
// Required for the internal timing of LVGL
lv_tick_inc(1);
}
}
如果提示找不到 lv_tick_inc(), 需要加上对头文件 lvgl/lvgl.h 的 include 。
这里涉及到三部分: 初始化 SPI 和对应的 GPIO, 初始化 ST7735, 最后才是 ST7735 的绘图函数. 。
初始化 GPIO, 这4个pin是需要声明为推挽输出的 PA2:BL, PA3:CS, PA4:DC(Data/Command), PA6:RESET 。
void APP_GPIO_Config(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2 | GPIO_Pin_3 | GPIO_Pin_4 | GPIO_Pin_6;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_SetBits(GPIOA, GPIO_Pin_2 | GPIO_Pin_3 | GPIO_Pin_4 | GPIO_Pin_6);
}
初始化 SPI, 这里还需要设置 PA5:SCK/SCL 和 PA7:SI/SDA 。
void APP_SPI_Config(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
SPI_InitTypeDef SPI_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_7;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_SetBits(GPIOA,GPIO_Pin_5 | GPIO_Pin_7);
SPI_InitStructure.SPI_Direction = SPI_Direction_1Line_Tx;
SPI_InitStructure.SPI_Mode = SPI_Mode_Master;
SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;
SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low;
SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge;
SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;
SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_16;
SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;
SPI_InitStructure.SPI_CRCPolynomial = 0;
SPI_Init(SPI1, &SPI_InitStructure);
SPI_Cmd(SPI1, ENABLE);
}
初始化 ST7735, 这部分已经在 st7735.c 中封装, 直接在main()中调用即可 。
ST7735_Init();
创建 ST7735 的区域绘图函数 。
void my_disp_flush(lv_disp_drv_t *disp, const lv_area_t *area, lv_color_t *color_p)
{
ST7735_WriteAddrWindow(area->x1, area->y1, area->x2, area->y2, (uint16_t *)color_p);
// Indicate you are ready with the flushing
lv_disp_flush_ready(disp);
}
对应的 ST7735_WriteAddrWindow() 函数实现, 因为来源是16bit, SPI接口是8bit, 每一次调用分别写入两次 。
void ST7735_WriteAddrWindow(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2, uint16_t *data)
{
uint32_t tmp, i;
if (x1 > x2)
{
tmp = x1; x1 = x2; x2 = tmp;
}
if (y1 > y2)
{
tmp = y1; y1 = y2; y2 = tmp;
}
tmp = (x2 - x1 + 1) * (y2 - y1 + 1);
ST7735_CS_LOW;
ST7735_SetAddrWindow(x1, y1, x2, y2);
while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET);
for(i = 0; i < tmp; i ++)
{
SPI_I2S_SendData(SPI1, (uint8_t)(*data >> 8));
while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_BSY) == SET);
SPI_I2S_SendData(SPI1, (uint8_t)(*data & 0xFF));
while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_BSY) == SET);
data++;
}
ST7735_CS_HIGH;
}
最小为1/10个屏幕尺寸所需数据大小 。
static lv_disp_draw_buf_t draw_buf;
// Declare a buffer for 1/10 screen size
static lv_color_t buf1[ST7735_WIDTH * ST7735_HEIGHT / 10];
// Descriptor of a display driver
static lv_disp_drv_t disp_drv;
在 main() 中进行初始化, 注意这部分官网给代码里的类型不太对, 这部分的代码已经修改 。
lv_init();
// Initialize the display buffer.
lv_disp_draw_buf_init(&draw_buf, buf1, NULL, ST7735_WIDTH * ST7735_HEIGHT / 10);
lv_disp_drv_init(&disp_drv); /*Basic initialization*/
disp_drv.flush_cb = my_disp_flush; /*Set your driver function*/
disp_drv.draw_buf = &draw_buf; /*Assign the buffer to the display*/
disp_drv.hor_res = ST7735_WIDTH; /*Set the horizontal resolution of the display*/
disp_drv.ver_res = ST7735_HEIGHT; /*Set the vertical resolution of the display*/
lv_disp_drv_register(&disp_drv); /*Finally register the driver*/
因为这个 ST7735 没有触屏功能, 所以输入读取函数就省了. 在 main() while(1)主循环中加上 lv_timer_handler() 。
while (1)
{
lv_timer_handler();
Delay_Ms(10);
}
经过以上的设置, LVGL就已经集成到项目中了, 可以运行LVGL自带的一些例子查看控件的显示效果 。
文字标签, 居中和滚动的效果 。
lv_example_label_1();
按钮效果 。
lv_example_btn_1();
以上LVGL整合示例的完整源代码已经提交到 GitHub: https://github.com/IOsetting/air32f103-template/tree/master/Examples/NonFreeRTOS/SPI/ST7735_LVGL 。
上面的例子是使用 SPI_I2S_SendData() 函数传输图像数据的, 可以修改为 DMA 传输, 因为传输方式的变化, 外设初始化和图像更新要做对应的调整, 这里没有使用中断. 。
GPIO 不变, 启用 SPI 的 DMA 。
在 APP_SPI_Config() 中启用SPI1 DMA 。
/* Enable SPI1 DMA TX request */
SPI_I2S_DMACmd(SPI1, SPI_I2S_DMAReq_Tx, ENABLE);
开启 DMA 时钟 。
void APP_DMA_Configuration(void)
{
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
}
因为DMA每次传输的数量在变化, 所以DMA的初始化在图像输出的方法里 。
图像更新方法的变化比较大, 这里需要根据输入的坐标, 计算实际的数据长度, 并对DMA进行初始化, 然后启动传输, 等待完成后关闭DMA 。
void my_disp_flush(lv_disp_drv_t *disp, const lv_area_t *area, lv_color_t *color_p)
{
uint16_t len;
DMA_InitTypeDef initStructure;
len = (area->x2 - area->x1 + 1) * (area->y2 - area->y1 + 1) * 2;
ST7735_CS_LOW;
ST7735_SetAddrWindow(area->x1, area->y1, area->x2, area->y2);
/* DMA1 Channel3 (triggered by SPI1 Tx event) Config */
DMA_DeInit(DMA1_Channel3);
initStructure.DMA_BufferSize = len;
initStructure.DMA_M2M = DMA_M2M_Disable;
initStructure.DMA_DIR = DMA_DIR_PeripheralDST;
initStructure.DMA_MemoryBaseAddr = (uint32_t)color_p;
initStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
initStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
initStructure.DMA_PeripheralBaseAddr = (uint32_t)&SPI1->DR;
initStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
initStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
initStructure.DMA_Priority = DMA_Priority_High;
initStructure.DMA_Mode = DMA_Mode_Normal;
DMA_Init(DMA1_Channel3, &initStructure);
// Start transfer
DMA_Cmd(DMA1_Channel3, ENABLE);
while (!DMA_GetFlagStatus(DMA1_FLAG_TC3));
DMA_ClearFlag(DMA1_FLAG_TC3);
DMA_Cmd(DMA1_Channel3, DISABLE);
ST7735_CS_HIGH;
// Indicate you are ready with the flushing
lv_disp_flush_ready(disp);
}
上面修改完成后, 再次运行LVGL示例, 会发现颜色不正确, 这是因为在DMA传输中, 将一个 16bit 强转为两个 8bit 了, ST7735收到的两个字节的顺序有变化, 需要编辑 lv_conf.h, 将 LV_COLOR_16_SWAP 设为 1 。
/*Swap the 2 bytes of RGB565 color. Useful if the display has an 8-bit interface (e.g. SPI)*/
#define LV_COLOR_16_SWAP 1
以上LVGL DMA 示例的完整源代码已经提交到 GitHub: https://github.com/IOsetting/air32f103-template/tree/master/Examples/NonFreeRTOS/SPI/ST7735_LVGL_DMA 。
进一步, 在 FreeRTOS 中运行 DMA 传输的 LVGL. 在 FreeRTOS 中 LVGL 的初始化是一样的, 有变化的是初始化的时间点, 还有延时函数的修改 。
新建 lvglTaskHandler() 用于处理LVGL初始化, 缓存初始化和执行benchmark, 并用固定间隔调用 lv_timer_handler() 。
static void lvglTaskHandler(void *pvParameters)
{
TickType_t xLastWakeTime = xTaskGetTickCount();
const TickType_t xPeriod = pdMS_TO_TICKS(10);
(void)(pvParameters); // Suppress "unused parameter" warning
ST7735_Init();
lv_init();
// Initialize the display buffer.
lv_disp_draw_buf_init(&draw_buf, buf1, NULL, ST7735_WIDTH * ST7735_HEIGHT / 10);
lv_disp_drv_init(&disp_drv); /*Basic initialization*/
disp_drv.flush_cb = my_disp_flush; /*Set your driver function*/
disp_drv.draw_buf = &draw_buf; /*Assign the buffer to the display*/
disp_drv.hor_res = ST7735_WIDTH; /*Set the horizontal resolution of the display*/
disp_drv.ver_res = ST7735_HEIGHT; /*Set the vertical resolution of the display*/
lv_disp_drv_register(&disp_drv); /*Finally register the driver*/
lv_demo_benchmark();
while (1)
{
lv_timer_handler();
vTaskDelayUntil(&xLastWakeTime, xPeriod);
}
}
在 main() 中创建任务, 栈深度1024, 需要 4KByte 内存 。
xTaskCreate(
lvglTaskHandler, // Task function point
"LVGL Task", // Task name
1024, // Stack size, each take 4 bytes(32bit)
NULL, // Parameters
LVGL_TASK_PRORITY, // Priority
NULL); // Task handler
更新图像的方法不变, 但是需要修改 ST7735 的延时函数, 修改 st7735.c, 引入 FreeRTOS.h , 将 Delay_Ms(ms); 替换为 vTaskDelay(ms),
在 FreeRTOSConfig.h 中, 设置系统最高的, 可以安全使用FreeRTOS方法的中断优先级为 1 。
#define configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY 0x01
因为 AIR32F103 的中断为 3 bit, 可用的优先级为 0 到 7, 这样对于优先级为 0 的中断是不受FreeRTOS控制的, 小于等于 1 的是受FreeRTOS控制的, 可以在中断处理中调用 FreeRTOS 的方法. 。
将TIM3的中断优先级设置为2 。
NVIC_InitStructure.NVIC_IRQChannel = TIM3_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
因为集成FreeRTOS, 加上运行 Benchmark, 256KB 的 Flash 容量就捉襟见肘了, 默认配置下编译出来会有300多KB, 需要进行压缩 。
首先确认编译参数已经优化, rules.mk 中, 优化项改为 -O3 或 -Os 。
# c flags
OPT ?= -O3
编辑 lv_conf.h 关闭一切不必要的组件, 使用尽可能小的字体(可以用10像素字体), 具体的改动可以参考示例代码. 。
以上LVGL+FreeRTO 示例的源代码已经提交到 GitHub: https://github.com/IOsetting/air32f103-template/tree/master/Examples/FreeRTOS/LVGL/ST7735_128x160 。
以上就是在 AIR32F103 上集成 LVGL 的步骤和说明, ST7735 也是常见模块. 对于 DMA 的例子, 可以进一步修改为使用中断判断 DMA 传输结束. 留有空再改了. 。
最后此篇关于AIR32F103(十)在无系统环境和FreeRTOS环境集成LVGL的文章就讲到这里了,如果你想了解更多关于AIR32F103(十)在无系统环境和FreeRTOS环境集成LVGL的内容请搜索CFSDN的文章或继续浏览相关文章,希望大家以后支持我的博客! 。
我开发了一个 Adobe AIR 应用程序,用户可以从我的网页上安装和启动它。如果用户计算机上尚未安装 AIR 应用程序,我将使用安装标志来安装它。在我对应用程序进行签名之前,此安装仅在 AIR
是否可以让我的 AIR 应用程序能够将自己添加到启动程序列表中? (我想在多个平台上进行这项工作 - 即 Windows、Mac 和 Linux)我将如何去做? 最佳答案 if (NativeAppl
我希望能够从Adobe AIR应用程序中启动第三方进程(实际上是命令行进程)。 AIR应用程序在运行时是否存在安全上下文,可以防止这种情况发生? 最佳答案 Adobe AIR 最需要的两个功能是从 A
我希望使用 Adobe Air 来可视化来自串行端口的信息。有没有办法在 Air 中天真地做到这一点?我假设不是。 如果是这种情况,我的最佳途径是创建一个本地应用程序,通过 TCP/IP 连接使串
Adobe 应用支持多核还是仍然使用单核? 如果我使用的是 Pentium 4 处理器 (3 ghz) 与双核处理器 (2.7 ghz) 相比,速度(应用程序的性能)是否会有所不同 编辑:如 Andr
我需要为 mac 和 Windows 开发一个桌面应用程序,使用 HTML JqueryMobile 和 CSS,我只是想确认 Adobe air 是否完全免费用于开发,并且想知道任何 IDE 支
我正在尝试将网络摄像头的视频和音频录制到存储在用户本地硬盘上的 FLV 文件中。我有此代码的一个版本,它使用 NetConnection 和 NetStream 通过网络将视频流式传输到 FMS (R
关闭。这个问题是opinion-based .它目前不接受答案。 想改进这个问题?更新问题,以便 editing this post 提供事实和引用来回答它. 8年前关闭。 Improve this
我的问题是 如何防止用户关闭应用程序? .我需要出现一条警告消息,询问用户是否真的想离开应用程序。 我的应用程序是在 中开发的Adobe AIR .请帮忙,我有麻烦了。 最佳答案 是的,我也发现了这个
我一直使用自行生成的证书为我的 Adobe AIR 应用程序签名,但现在我想要看起来更正式的证书。我应该从哪里获得什么样的商业证书? 一如既往,感谢您的帮助! 最佳答案 经过一番寻找,我在 AIR
关闭。这个问题不符合Stack Overflow guidelines .它目前不接受答案。 想改进这个问题?将问题更新为 on-topic对于堆栈溢出。 8年前关闭。 Improve this qu
Adobe Air 究竟是什么?我看到很多人在谈论它,我什至看到了它的应用程序,但我仍然不完全确定它的独特之处或它与其他语言的不同之处。有人可以从程序员的角度给我简明的版本吗? 编辑: 我不熟悉 Fl
我想显示位于我的 AIR 应用程序的 app-storage 目录中的图像。此图像必须显示在 html 文本区域中,其中 html 文本来自本地嵌入式 sqlite 数据库。 当我输入以下代码时: i
我刚开始学习 Adobe AIR。我想通过引用示例应用程序我会学得很快。您能告诉我任何 Adobe AIR 开源应用程序吗? 感谢您的帮助。 最佳答案 超过 20 个示例应用程序(来自 Ado
创建 adobe air 文件后,我可以使用 winrar 或任何其他存档程序打开它,并查看所有内容,包括我的程序的代码。 有办法避免这种情况吗?某种加密或其他什么? 非常感谢。 最佳答案 没有。 “
已结束。此问题正在寻求书籍、工具、软件库等的推荐。它不满足Stack Overflow guidelines 。目前不接受答案。 我们不允许提出寻求书籍、工具、软件库等推荐的问题。您可以编辑问题,以便
我们的代码签名证书最近过期了。它已更新,但现在每当我尝试使用更新的证书打包应用程序(无论我是否尝试迁移过期的证书),安装后,每当我尝试运行应用程序时都会收到以下消息: “此应用程序的此安装已损坏。请尝
我遇到了这个问题,我需要一个简单的医疗设备来将一串逗号分隔的数字输出到我的 Adobe Air 应用程序。我很高兴能与 Web 服务以外的东西交谈! 所以,我得到了一个 USB 转串口适配器,插入
是否可以在不安装的情况下运行 Adobe AIR 应用程序? AIR 运行时已经安装,我们需要一个解决方案来控制应用程序的“安装”——所以我们需要的是运行一个 adobe air 应用程序,该应用
我们有一个 Adobe Air 应用程序,可以将大量图像下载到应用程序存储中。我扫描了文档并没有发现任何迹象,但我想我会仔细检查:任何人都知道是否有可能看到用户在他们的 HD 上有多少可用存储空间
我是一名优秀的程序员,十分优秀!