2024年1月


主要思路是:
1.使用UIBezierPath绘制一个六边形路径
2.创建一个CAShapeLayer图层,将这个六边形path设置到CAShapeLayer属性上。然后设置fillColor为透明,strokeColor为黑色,lineWidth为5.0
3.创建一个CAGradientLayer渐变色图层,并将它的渐变类型设置成kCAGradientLayerConic以圆心为起点,以圆边为中点,并沿着圆旋转一周
4.设置CAGradientLayer.mask = CAShapeLayer, 得出一个被CAShapeLayer切割后的六边形
5.给CAGradientLayer的colors内容添加动态,让渐变图层的内容旋转,从而得到六边形渐变色旋转的结果



创建一个路径,也可以根据自己的需要进行创建,这里创建的是一个六边形
CGFloat squareSize =MIN(imageSize.width, imageSize.height);
UIBezierPath
* path =[UIBezierPath bezierPath];
[path moveToPoint:CGPointMake((sin(M_1_PI
/ 180 * 60)) * (squareSize / 2), (squareSize / 4))];
[path addLineToPoint:CGPointMake((squareSize
/ 2), 0)];
[path addLineToPoint:CGPointMake(squareSize
- ((sin(M_1_PI / 180 * 60)) * (squareSize / 2)), (squareSize / 4))];
[path addLineToPoint:CGPointMake(squareSize
- ((sin(M_1_PI / 180 * 60)) * (squareSize / 2)), (squareSize / 2) + (squareSize / 4))];
[path addLineToPoint:CGPointMake((squareSize
/ 2), squareSize)];
[path addLineToPoint:CGPointMake((sin(M_1_PI
/ 180 * 60)) * (squareSize / 2), (squareSize / 2) + (squareSize / 4))];
[path closePath];

将创建好的UIBezierPath传递给CAShapeLayer图层中,让图层拥有path的图案
CAShapeLayer *pathLayer = [CAShapeLayer layer];
pathLayer.frame = CGRectMake(0, 5, self.bounds.size.width, self.bounds.size.height);
pathLayer.path = path.CGPath;
pathLayer.strokeColor = [UIColor blackColor].CGColor;
pathLayer.fillColor = [UIColor clearColor].CGColor;
pathLayer.lineWidth = 5.0;
pathLayer.lineJoin = kCALineJoinRound;

创建一个渐变色图层
// 创建渐变图层
CAGradientLayer *gradientLayer = [CAGradientLayer layer];
gradientLayer.frame = self.bounds;
gradientLayer.type = kCAGradientLayerConic;
// 定义颜色数组,中间白色,两边透明
NSArray *colors = @[(id)[UIColor colorWithWhite:1 alpha:1].CGColor,
                    (id)[UIColor colorWithWhite:0.8 alpha:0.8].CGColor,
                    (id)[UIColor colorWithWhite:0.6 alpha:0.6].CGColor,
                    (id)[UIColor colorWithWhite:0.5 alpha:0].CGColor,
                    (id)[UIColor colorWithWhite:0 alpha:0].CGColor,
                    (id)[UIColor colorWithWhite:0.5 alpha:0].CGColor,
                    (id)[UIColor colorWithWhite:0.6 alpha:0.6].CGColor,
                    (id)[UIColor colorWithWhite:0.8 alpha:0.8].CGColor,
                    (id)[UIColor colorWithWhite:1 alpha:1].CGColor];
gradientLayer.colors = colors;
gradientLayer.startPoint = CGPointMake(0.5, 0.5);
gradientLayer.endPoint = CGPointMake(0, 0.5);
// 设置颜色位置
gradientLayer.locations = @[@0.0, @0.05, @0.1, @0.15, @0.5, @0.85, @0.9, @0.95, @1.0];

将渐变图层添加到View的图层上,然后使用Shape图层进行mask切割,得出想要的图案。
mask的的原理:对应设置的图层中,如果像素是黑色的,其对应的下面的图层内容就显示,如果某部分图层像素是透明的,那么透明像素下面的图层内容就不展示。
[self.layer addSublayer:self.gradientLayer];
self.gradientLayer.mask = self.pathLayer;

开启渐变图层内容动画
将图层中的endPoint随时间进行线性移动,从而实现图层固定,里面的内容随时间进行动画。
- (void)createTimer {
    gcdTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(0, 0));
    dispatch_source_set_timer(gcdTimer, DISPATCH_TIME_NOW, 0.02 * NSEC_PER_SEC, 0.01 * NSEC_PER_SEC);
    dispatch_source_set_event_handler(gcdTimer, ^{
        dispatch_async(dispatch_get_main_queue(), ^{
            [self handleTimerEvent];
        });
    });
    dispatch_resume(gcdTimer);
}

- (void)handleTimerEvent {
    CGFloat speed = 0.04;
    CGPoint endPoint = self.gradientLayer.endPoint;
    if (endPoint.x <= 0 && endPoint.y > 0 && !self.leftMove) {
        self.bottomMove = YES;
        self.topMove = NO;
        self.gradientLayer.endPoint = CGPointMake(endPoint.x, endPoint.y-speed);
    } else if (endPoint.x < 1 && endPoint.y <= 0 && !self.topMove) {
        self.leftMove = YES;
        self.rightMove = NO;
        self.gradientLayer.endPoint = CGPointMake(endPoint.x+speed, endPoint.y);
    } else if (endPoint.x >= 1 && endPoint.y < 1 && !self.rightMove) {
        self.topMove = YES;
        self.bottomMove = NO;
        self.gradientLayer.endPoint = CGPointMake(endPoint.x, endPoint.y+speed);
    } else if (endPoint.x >= 0 && endPoint.y >= 1 && !self.bottomMove) {
        self.rightMove = YES;
        self.leftMove = NO;
        self.gradientLayer.endPoint = CGPointMake(endPoint.x-speed, endPoint.y);
    }
}


最终得到一个六边形,它的边框颜色是渐变色,并且这个渐变色沿着六边形旋转。



JIT、逃逸分析、锁消除、栈上分配和标量替换等都属于 JVM 的优化手段,JVM 优化手段是指在运行 Java 程序时,通过对字节码的编译和执行过程进行优化,以提升程序的性能和效率。

JVM 优化手段主要有以下几个:

  1. JIT(Just-In-Time,即时编译)
    :是一种在程序运行时将部分热点代码编译成机器代码的技术,以提高程序的执行性能的机制。
  2. 逃逸分析
    :用于确定对象动态作用域是否超过当前方法或线程,通过逃逸分析,编译器可以决定一个对象的作用范围,从而进行相应的优化,但确定对象没有逃逸时,可以进行以下优化:
    1. 栈上分配
      :如果编译器可以确定一个对象不会逃逸出方法,它可以将对象分配在栈上而不是堆上。在栈上分配的对象在方法返回后就会自动销毁,不需要进行垃圾回收,提高了程序的执行效率。
    2. 锁消除
      :如果对象只在单线程中使用,那么同步锁可能会被消除,提高程序性能。
    3. 标量替换
      :将原本需要分配在堆上的对象拆解成若干个基础数据类型存储在栈上,进一步减少堆空间的使用。
  3. 字符串池(String Pool)优化
    :JVM 通过共享字符串常量,重用字符串对象,以减少内存占用和提升字符串操作的性能。

1.JIT优点和热点代码

JIT 优点包含以下两个:

  1. 性能优化
    :由于编译成本地机器代码,程序的执行速度通常比解释性执行或预编译的代码要快得多。
  2. 平台无关性
    :JIT 编译器可以根据不同的硬件平台生成不同的机器代码,使得相同的程序可以在不同的计算机上运行,而无需重新编写。

什么是热点代码?

在 HotSpot 虚拟机中,热点代码(Hot Code)是指那些被频繁执行的代码。
热点代码的执行次数在不同的 JDK 版本和不同的 JVM 中是不同的,例如,它在 JDK 21 Client 模式下为 1500 次,Server 模式下为 10000 次,这个值可以通过 JVM 参数设置。
通常来说,热点代码的识别基于以下两种策略:

  1. 方法调用次数
    :当一个方法被调用一定次数后,会被视为热点代码并触发即时编译。这个次数在不同 JDK 版本中可能有所变化,并且可以通过 JVM 参数 -XX:CompileThreshold 进行设置。
  2. 回边计数
    :对于循环体等热点区域,通过统计从循环体返回到循环条件检查点的次数(即回边次数),达到一定次数也会触发即时编译。同样,这个阈值也可以通过 JVM 参数 -XX:OnStackReplacePercentage 进行设置。回边计数器有一个计算公式【回边计数器阈值=方法调用计数器阈值 * (OnStackReplacePercentage - InterpreterProfilePercentage)】,通过计算,在 JDK 21 Server 模式下,虚拟机回边计数器的阈值为 10700【10000*(140-33)】。

可以使用 java -XX:+PrintFlagsFinal -version 命令查看 JVM 默认配置。

2.栈上分配 VS 标量替换

栈上分配和标量替换是编译器的两种优化技术,它们虽然有一些相似之处,但并不完全相同。

  • 栈上分配(Stack Allocation)
    :一种优化技术,它将对象分配在栈上而不是堆上。这种技术适用于编译器可以确定对象不会逃逸出方法,并且对象的生命周期在方法内部结束的情况。通过在栈上分配对象,可以避免在堆上进行内存分配和垃圾回收的开销,从而提高程序的性能和内存使用效率。
  • 标量替换(Scalar Replacement)
    :与栈上分配类似,也是一种优化技术。它将一个复杂对象拆分成独立的成员变量,使其成为基本类型或基本类型数组的局部变量。这种技术适用于编译器可以确定对象的成员变量不会逃逸的情况。标量替换可以提供更细粒度的控制,使得编译器可以对独立的成员变量进行更精细的优化,例如寄存器分配和代码优化。

也就是说栈上分配,只是将对象从堆上分配到栈上了;而标量替换是更进一步的优化技术,将对象拆解成基本类型分配到栈上了。

2.1 锁消除代码演示

锁消除(Lock Elimination)也叫做同步消除,是一种编译器优化技术,它可以消除对于变量的不必要的锁定操作。锁消除的目的是减少锁的开销,提高程序的性能。
例如以下代码:

public void method() {
    Object lock = new Object();
    synchronized(lock){
        System.out.println("www.javacn.site");
    }
}

而锁消除之后的代码如下:

public void method(){
    System.out.println("www.javacn.site");
}

2.2 标量替换代码演示

未优化前的代码如下:

private static class Point {
    private int x;
    private int y;
}
public static void main(String[] args) {
    Point point = createPoint(10, 20);
    int sum = point.x + point.y;
    System.out.println("Sum: " + sum);
}
private static Point createPoint(int x, int y) {
    Point point = new Point();
    point.x = x;
    point.y = y;
    return point;
}

标量替换优化后的代码如下:

public static void main(String[] args) {
    int x = 10;
    int y = 20;
    int sum = x + y;
    System.out.println("Sum: " + sum);
}

通过逃逸分析的优化能够减少垃圾回收的压力、减少内存分配和释放带来的性能损耗,并且有可能减少对锁的依赖,以及实现标量替换等,从而整体上提升了应用程序的运行效率。

课后思考

Java 为什么不把所有代码提前都编译成二进制的机器码呢?这样岂不是运行更快?新 Java 虚拟机 GraalVM 中的 AOT 和 JIT 又有什么区别呢?

本文已收录到我的面试小站
www.javacn.site
,其中包含的内容有:Redis、JVM、并发、并发、MySQL、Spring、Spring MVC、Spring Boot、Spring Cloud、MyBatis、设计模式、消息队列等模块。

前言

近日心血来潮想做一个开源项目,目标是做一款可以适配多端、功能完备的模板工程,包含后台管理系统和前台系统,开发者基于此项目进行裁剪和扩展来完成自己的功能开发。

本项目为前后端分离开发,后端基于
Java21

SpringBoot3
开发,后端使用
Spring Security

JWT

Spring Data JPA
等技术栈,前端提供了
vue

angular

react

uniapp

微信小程序
等多种脚手架工程。

本文主要介绍在
SpringBoot3
项目中如何整合
springdoc-openapi
实现自动生成在线接口文档,JDK版本是
Java21

项目地址:
https://gitee.com/breezefaith/fast-alden

相关技术简介

OpenAPI

OpenAPI 规范(OAS),是定义一个标准的、与具体编程语言无关的RESTful API的规范。OpenAPI 规范使得人类和计算机都能在“不接触任何程序源代码和文档、不监控网络通信”的情况下理解一个服务的作用。如果您在定义您的 API 时做的很好,那么使用 API 的人就能非常轻松地理解您提供的 API 并与之交互了。

如果您遵循 OpenAPI 规范来定义您的 API,那么您就可以用文档生成工具来展示您的 API,用代码生成工具来自动生成各种编程语言的服务器端和客户端的代码,用自动测试工具进行测试等等。

参考文档:OpenAPI 规范 (中文版)
https://openapi.apifox.cn/

Swagger

Swagger是一套围绕 Open API 规范构建的开源工具,可以帮助设计,构建,记录和使用 REST API。

Swagger工具包括的组件:

  • Swagger Editor :基于浏览器编辑器,可以在里面编写 Open API规范。类似 Markdown 具有实时预览描述文件的功能。
  • Swagger UI:将 Open API 规范呈现为交互式 API 文档。用可视化UI 展示描述文件。
  • Swagger Codegen:将 OpenAPI 规范生成为服务器存根和客户端库。通过 Swagger Codegen 可以将描述文件生成 html 格式和 cwiki 形式的接口文档,同时也可以生成多种言语的客户端和服务端代码。
  • Swagger Inspector:和 Swagger UI 有点类似,但是可以返回更多信息,也会保存请求的实际参数数据。
  • Swagger Hub:集成了上面所有项目的各个功能,你可以以项目和版本为单位,将你的描述文件上传到 Swagger Hub 中。在 SwaggerHub 中可以完成上面项目的所有工作,需要注册账号,分免费版和收费版。

使用 Swagger,就是把相关的信息存储在它定义的描述文件里面(yml 或 json 格式),再通过维护这个描述文件可以去更新接口文档,以及生成各端代码。

官方文档:
https://swagger.io/

Springfox

Springfox是一套可以帮助Java开发者自动生成API文档的工具,它是基于Swagger 2.x基础上开发的,它遵循的是OpenAPI2.0(即Swagger2.0规范)。Swagger已经成为了RESTful API文档生态系统的事实标准,而Springfox是一个用于集成Swagger2.x到Spring应用程序中的库。而且Springfox提供了一些注解来描述API接口、参数和返回值,并根据这些信息生成Swagger UI界面,从而方便其他开发人员查看和使用您的API接口。

此外,Springfox还支持自动生成API文档和代码片段,简化了开发人员的工作量。除了集成Swagger 2.x,Springfox还提供了一些额外功能,例如自定义Swagger文档、API版本控制、请求验证等等。这些功能使得Springfox可以胜任各种类型和规模的应用程序,同时还可以提高代码质量和开发效率。

总之,Springfox是一个非常有用的工具,它可以帮助Java开发者快速、简单地集成Swagger2.x,并为他们的应用程序生成高质量的API文档。无论您开发的是大型企业应用程序还是小型服务,使用Springfox都能够提高团队的生产力和代码质量。

官方文档:
https://springfox.github.io/springfox/

springdoc

SpringDoc是基于OpenAPI 3.0规范构建的,因此推荐在Spring Boot 2.4及以上版本中使用springdoc-openapi-ui库来集成Swagger3.x。在这些版本中,springdoc-openapi-ui库已被广泛应用,并且得到了社区的大力支持和推广。而在Spring Boot 2.3及其以下版本,可以使用springfox-boot-starter库来集成Swagger2.x。

SpringDoc有着更加先进的技术架构和更好的扩展性,使得其逐渐取代了springfox-boot-starter工具包,成为了当前Spring Boot生态中最受欢迎的API文档工具之一。同时springdoc-openapi-ui还拥有更为完善的开发文档和社区支持,从而吸引了越来越多的开发者加入到这个项目中。因此作为一个Spring Boot开发者,如果想要快速、方便地生成符合OpenAPI 3.0规范的接口文档,建议使用springdoc-openapi-ui这个优秀的工具。

官方文档:
https://springdoc.org/

swagger2与swagger3常用注解对比

swagger2 swagger3 注解位置
@Api @Tag(name = “接口类描述”) Controller 类
@ApiOperation @Operation(summary =“接口方法描述”) Controller 方法
@ApiImplicitParams @Parameters Controller 方法
@ApiImplicitParam @Parameter(description=“参数描述”) Controller 方法的 @Parameters 里
@ApiParam @Parameter(description=“参数描述”) Controller 方法的参数上
@ApiIgnore @Parameter(hidden = true) 或 @Operation(hidden = true) 或 @Hidden -
@ApiModel @Schema DTO类上
@ApiModelProperty @Schema DTO属性上

实现步骤

引入maven依赖


pom.xml
中添加
springdoc-openapi-starter-webmvc-ui
以及相关依赖。

<dependencies>
  <dependency>
    <groupId>org.springdoc</groupId>
    <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
    <version>2.3.0</version>
  </dependency>
  <!-- 项目中使用了spring-security时可以引入此依赖 -->
  <dependency>
    <groupId>org.springdoc</groupId>
    <artifactId>springdoc-openapi-security</artifactId>
    <version>1.7.0</version>
  </dependency>
  <!-- 如果使用的是spring webflux而非spring-webmvc,则需要将springdoc-openapi-starter-webmvc-ui改为如下依赖 -->
  <!-- <dependency>
    <groupId>org.springdoc</groupId>
    <artifactId>springdoc-openapi-starter-webflux-ui</artifactId>
    <version>2.3.0</version>
  </dependency> -->
</dependencies>

修改配置文件


application.yml
中可以自定义
api-docs

swagger-ui
的访问路径。

springdoc:
  api-docs:
    path: /v3/api-docs
  swagger-ui:
    path: /swagger-ui.html

设置
api-docs

swagger-ui
访问权限

如果项目中启用了权限控制,需要合理设置
api-docs

swagger-ui
相关资源的访问权限。比如笔者使用的
spring-security
,将
api-docs

swagger-ui
相关资源设置为允许匿名访问,不需要认证授权。

@Configuration
public class SecurityConfig {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        // 将api-docs和swagger-ui相关资源设置为允许匿名访问
        http.authorizeHttpRequests((authorizationManagerRequestMatcherRegistry) -> {
			authorizationManagerRequestMatcherRegistry.requestMatchers("/v3/api-docs").permitAll();
			authorizationManagerRequestMatcherRegistry.requestMatchers("/v3/api-docs/**").permitAll();
			authorizationManagerRequestMatcherRegistry.requestMatchers("/swagger-ui.html").permitAll();
			authorizationManagerRequestMatcherRegistry.requestMatchers("/swagger-ui/**").permitAll();
        
            // 其余资源登录后方可访问
            authorizationManagerRequestMatcherRegistry.anyRequest().authenticated();
        });
    	
        // 其余spring security配置已省略
        // ...
        
        return http.build();
    }
}

定义springdoc配置类

@Configuration
public class SpringDocConfig {
    /**
     * 一个自定义的 OpenAPI 对象
     *
     * @return 一个自定义的 OpenAPI 对象
     */
    @Bean
    public OpenAPI customOpenApi() {
        return new OpenAPI()
        .components(new Components()
                    // 设置 spring security jwt accessToken 认证的请求头 Authorization: Bearer xxx.xxx.xxx
                    .addSecuritySchemes("openApiSecurityScheme", new SecurityScheme()
                                        .type(SecurityScheme.Type.HTTP)
                                        .bearerFormat("JWT")
                                        .in(SecurityScheme.In.HEADER)
                                        .name("Authorization")
                                        .scheme("Bearer")))
        // 设置标题、版本等信息
        .info(new Info()
              .title("Fast Alden权限管理系统")
              .version("0.0.1-SNAPSHOT")
              .description("")
              .license(new License()
                       .name("Apache 2.0")
                       .url("https://www.apache.org/licenses/LICENSE-2.0.html")));
    }
}

在上述代码中定义了一个key为
openApiSecurityScheme

SecuritySchemes
,在后续章节的Controller类中使用。

修改Controller类和实体类

在Controller类和实体类中添加swagger相关注解。

  • @Tag 用于标识controller
  • @Operation 用于标识方法
  • @Schema 用于标识实体类和实体类的属性
  • @ApiResponse 用于标识请求的响应
  • @Parameters和@Parameter 用于标识请求参数,@Parameter的name需要和变量的命名一致,@Parameter要放到函数形参前面

以下代码中
@Operation
注解通过
security
属性指定认证方式,
openApiSecurityScheme
已在上文springdoc配置类中声明。

  1. SysUserController.java
// SysUserController.java
@Tag(name = "SysUserController", description = "后台用户管理")
@RestController
@RequestMapping("/user")
public class SysUserController {
    @Resource
    private SysUserService userService;

    @Operation(summary = "根据ID查询", security = @SecurityRequirement(name = "openApiSecurityScheme"))
    @GetMapping("/retrieve/{id}")
    public SysUser retrieve(@PathVariable("id") Long id) {
        return userService.retrieve(id);
    }

    @Operation(summary = "创建用户", security = @SecurityRequirement(name = "openApiSecurityScheme"))
    @PostMapping("/create")
    public Long create(@RequestBody SysUser user) {
        return userService.create(user).getId();
    }

    @Operation(summary = "修改用户", security = @SecurityRequirement(name = "openApiSecurityScheme"))
    @PutMapping("/update")
    public void update(@RequestBody SysUser user) {
        userService.update(user);
    }

    @Operation(summary = "删除用户", security = @SecurityRequirement(name = "openApiSecurityScheme"))
    @DeleteMapping("/remove")
    public void remove(@RequestParam("ids") List<Long> ids) {
        userService.remove(ids);
    }
}

  1. SysUser.java
// SysUser.java
/**
 * 用户实体类
 */
@Getter
@Setter
@Schema(description = "用户")
public class SysUser {
    @Schema(description = "用户ID")
    private Long id;

    @Schema(description = "用户名")
    private String username;

    @Schema(description = "密码")
    private String password;

    @Schema(description = "电话")
    private String phone;

    @Schema(description = "个人介绍")
    private String introduce;

    @Schema(description = "所属部门ID")
    private Long departmentId;
}

查看效果

  1. 访问
    http://localhost:8080/v3/api-docs
    可获取JSON格式的API文档。

image

  1. 访问
    http://localhost:8080/swagger-ui.html
    可直接在线测试API,在
    Authorize
    弹窗中可以填入token用于模拟在线用户。

image

image

总结

本文简单介绍了一下OpenAPI、Swagger、Springfox和SpringDoc的相关概念,以及详细介绍了
SpringBoot3
整合
SpringDoc
的过程,如有错误,还望批评指正。

在后续实践中我也是及时更新自己的学习心得和经验总结,希望与诸位看官一起进步。

1、准备材料

正点原子stm32f407探索者开发板V2.4

STM32CubeMX软件(
Version 6.10.0

keil µVision5 IDE(
MDK-Arm

ST-LINK/V2驱动

野火DAP仿真器

XCOM V2.6串口助手

2、实验目标

使用STM32CubeMX软件配置STM32F407开发板
USB_OTG_FS为工作在Mass Storage Class(大容量存储类)模式下的USB_DEVICE(USB从机),使其作为SD卡读卡器在Windows系统文件资源管理器中直接对SD卡进行读写操作

3、实验流程

3.0、前提知识

关于USB的相关知识请读者阅读
STM32CubeMX教程29 USB_HOST - 使用FatFs文件系统读写U盘
实验“3、USB概述”小节内容,USB_SALVE 从机接口硬件原理图请读者阅读其“4.0、前提知识”小节内容

当USB工作在USB_DEVICE时可以将其模式配置为以下6种模式中的任何一种,
本实验只会介绍其中的大容量存储设备,其他的一概不涉及
,具体的6种模式如下图所述

将USB设备接口配置工作在Mass Storage Class模式下,主要是为了将没有USB接口的大容量的存储设备(eg:SD卡)通过该接口,利用USB连接与USB主机之间建立联系,然后便可以通过USB主机对该大容量存储设备进行控制

对于USB_OTG_FS工作在任意USB外设模式下来说,在CubeMX中一般需要对Configuration下Parameter Settings、Device Descriptor 和User Constants三个参数页面参数进行配置(虽然这些参数一般无需修改,保持默认即可)

其中Parameter Settings 选项卡下的参数会根据不同的外设工作模式出现对应该外设的一些重要参数设置,不同外设出现的参数不尽相同

Device Descriptor 选项卡下的参数则较为固定
,不同外设之间往往只会改变Device Descriptor FS下的参数,该选项卡下的所有参数主要用于描述该USB外设,正因为其参数则较为固定,因此这里笔者直接列出了各个参数的含义,在之后的其他外设实验中便不再重复说明,具体含义如下图所示

User Constants 选项卡主要用于设定一些用户需要的常量参数,增加之后会以宏定义的形式出现在main.h文件中

在完成该实验之后读者也可以验证下设备描述符中内容和Windwos中读取到的USB设备描述符是否一致

右键单击弹出的U盘驱动器,单击属性,双击硬件选项卡中的STM Product USB Device,在弹出的页面中单击详细信息,最后在属性里面找到父系,可以在下面找到设备描述中的VID和PID,将其转换为十进制之后发现和我们设定值一致,具体如下图所示

3.1、CubeMX相关配置

3.1.0、工程基本配置

打开STM32CubeMX软件,单击ACCESS TO MCU SELECTOR选择开发板MCU(选择你使用开发板的主控MCU型号),选中MCU型号后单击页面右上角Start Project开始工程,具体如下图所示

开始工程之后在配置主页面System Core/RCC中配置HSE/LSE晶振,在System Core/SYS中配置Debug模式,具体如下图所示

详细工程建立内容读者可以阅读“
STM32CubeMX教程1 工程建立

3.1.1、时钟树配置

将时钟树中48MHz时钟配置为48MHz,也即将Main PLL(主锁相环)的Q参数调节为7,其他HCLK、PCLK1和PCLK2时钟仍然设置为STM32F407能达到的最高时钟频率,具体如下图所示

3.1.2、外设参数配置

本实验需要需要初始化USART1作为输出信息渠道,具体配置步骤请阅读“
STM32CubeMX教程9 USART/UART 异步通信

另外由于需要通过SDIO读写SD卡,USB_OTG_FS工作在从机模式下,因此还需要配置SDIO和USB_OTG_FS

单击Pinout & Configuration页面左边功能分类栏目中Connectivity/SDIO,将其模式配置为4位宽总线SD卡,在下方参数配置Configuration/Parameter Settings中将参数
SDIOCLK clock divide factor
配置为4即可,具体参数含义请读者阅读
STM32CubeMX教程27 SDIO - 读写SD卡
实验内容,具体配置如下图所示

单击Pinout & Configuration页面左边功能分类栏目中
Connectivity/USB_OTG_FS,将其模式配置为仅从机(Device_Only),其他所有参数保持默认即可
,具体配置如下图所示

单击Pinout & Configuration页面左边功能分类栏目中
Middleware and Software Packs/USB DEVICE,将其模式配置为Mass Storage Class(大容量存储类),其他所有参数保持默认即可
,具体配置如下图所示

USBD_MAX_NUM_INTERFACES
(支持的最大接口数):可选1 ~ 255,不应超过可用内存的总大小

USBD_MAX_NUM_CONFIGURATION
(支持的最大配置数):可选1 ~ 255,不应超过可用内存的总大小

USBD_MAX_STR_DESC_SIZ
(字符串描述符的最大大小) :可选1 bytes ~ 64 Kbytes,用于设定Device Descriptor页面中对该USB设备的一些描述字符串最大长度

USBD_SELF_POWERED
(启用自供电) :可选Enable、Disable,此处选择Enable表示USB设备有自己的电源供应,不需要从USB总线上获取电力

USBD_DEBUG_LEVEL
(USBD调试级别):可选0、1、2、3,具体调试级别如下所示

  1. 0 : No debug message is shown
  2. 1 : only User message are shown
  3. 2 : User + Error messages are shown
  4. 3 : All message and interal debug message are shown

MSC_MEDIA_PACKET
(媒体I/O缓冲区大小):可选1 bytes ~ 32 Kbytes,在USB大容量存储设备中,数据传输通常是以数据包为单位进行的,该宏定义了每个数据包的大小

3.1.3、外设中断配置

当在Middleware and SoftwarePacks中配置了USB_DEVICE的模式不为Disable时,便会自动开启USB_OTG的全局中断,且不可关闭,用户配置合适的中断优先级即可,具体配置如下图所示

3.2、生成代码

3.2.0、配置Project Manager页面

单击进入Project Manager页面,在左边Project分栏中修改工程名称、工程目录和工具链,然后在Code Generator中勾选“Gnerate peripheral initialization as a pair of 'c/h' files per peripheral”,最后单击页面右上角GENERATE CODE生成工程,具体如下图所示

详细Project Manager配置内容读者可以阅读“
STM32CubeMX教程1 工程建立
”实验3.4.3小节

3.2.1、设初始化调用流程

打开生成的工程,观察目录结构,由于启用了USB_DEVICE,因此在工程目录种增加了USB设备库文件目录USB_Device_Library,在USB_DEVICE/Target目录下增加了usbd_conf.c参数配置文件,在USB_DEVICE/App目录下增加了usb_device.c初始化文件、usbd_desc.c描述文件和usbd_storage_if.c外设接口文件

其中USB_Device_Library目录下所有文件、usbd_conf.c、usbd_desc.c和usb_device.c几个文件均不需要在生成的工程代码中做任何修改,
用户唯一需要修改的是在usbd_storage_if.c外设接口文件中实现的大容量存储设备的接口函数
,如下图所示为USB_DEVICE工作在Mass Storage Class下生成工程文件目录

究竟是在哪里将需要我们在usbd_storage_if.c外设接口文件中重新实现的接口函数与USBD实例化对象联系起来的呢?

在 usb_device.c 文件中只有MX_USB_DEVICE_Init()一个函数,该函数体内执行了四个函数对USB_DEVICE进行了初始化和启动操作

其中通过调用USBD_MSC_RegisterStorage(&hUsbDeviceFS, &USBD_Storage_Interface_fops_FS)函数,将一个USBD_StorageTypeDef类型的结构体与USBD实例化对象联系了起来,该USBD_StorageTypeDef类型的结构体中包含的正是usbd_storage_if.c外设接口文件中的所有接口函数指针

3.2.2、外设中断调用流程

未使用外设任何中断

3.2.3、添加其他必要代码

STM32CubeMX工程生成工程代码后,读者应注意手动修改MX_SDIO_SD_Init()函数中SD卡数据总线宽度从默认的4位手动修改为1位,否则SD卡将初始化失败

根据上面的描述,唯一需要用户修改的地方是usbd_storage_if.c文件中的7个函数,与W25Q128芯片移植FatFs时类似,读者可以参考
STM32CubeMX教程26 FatFs 文件系统 - W25Q128读写
实验“3.2、生成代码“小节内容,如下所示为重新实现后的七个函数源代码

/*usbd_storage_if.c*/

/*初始化函数无需修改,因为SD卡初始化在SDIO初始化函数中已完成*/
int8_t STORAGE_Init_FS(uint8_t lun)
{
  /* USER CODE BEGIN 2 */
 UNUSED(lun);

  return (USBD_OK);
  /* USER CODE END 2 */
}

/*获取存储介质容量*/
int8_t STORAGE_GetCapacity_FS(uint8_t lun, uint32_t *block_num, uint16_t *block_size)
{
  /* USER CODE BEGIN 3 */
	HAL_SD_CardInfoTypeDef cardInfo;
	//使用SDIO库函数
	HAL_StatusTypeDef res = HAL_SD_GetCardInfo(&hsd, &cardInfo);
	if(res == HAL_OK)
	{
		*block_num  = cardInfo.BlockNbr;    //块的个数
		*block_size = cardInfo.BlockSize;   //块大小=512字节
	}
	else
	{
		*block_num  = STORAGE_BLK_NBR;	    //0x10000
		*block_size = STORAGE_BLK_SIZ;      //块大小=512字节
	}
	
	return (USBD_OK);
  /* USER CODE END 3 */
}

/*返回存储介质是否准备好,无需修改*/
int8_t STORAGE_IsReady_FS(uint8_t lun)
{
  /* USER CODE BEGIN 4 */
  UNUSED(lun);

  return (USBD_OK);
  /* USER CODE END 4 */
}

/*返回存储介质是否写保护,,无需修改*/
int8_t STORAGE_IsWriteProtected_FS(uint8_t lun)
{
  /* USER CODE BEGIN 5 */
  UNUSED(lun);

  return (USBD_OK);
  /* USER CODE END 5 */
}

/*读取存储介质*/
int8_t STORAGE_Read_FS(uint8_t lun, uint8_t *buf, uint32_t blk_addr, uint16_t blk_len)
{
  /* USER CODE BEGIN 6 */
	//读取超时时间10000ms
	uint32_t Timeout = 10000;  
	HAL_StatusTypeDef res = HAL_OK;
	res = HAL_SD_ReadBlocks(&hsd, buf, blk_addr, blk_len, Timeout);
	HAL_SD_CardStateTypeDef status = HAL_SD_CARD_RECEIVING;
	
	if(res == HAL_OK)
	{
		//等待传输完成
		while(status != HAL_SD_CARD_TRANSFER)   
			status = HAL_SD_GetCardState(&hsd);
		return (USBD_OK);
	}
	else
		return (USBD_FAIL);
  /* USER CODE END 6 */
}

/*向存储介质写入数据*/
int8_t STORAGE_Write_FS(uint8_t lun, uint8_t *buf, uint32_t blk_addr, uint16_t blk_len)
{
  /* USER CODE BEGIN 7 */
	//写入超时时间10000ms
	uint32_t Timeout = 10000;   
	HAL_StatusTypeDef res = HAL_OK;
	res = HAL_SD_WriteBlocks(&hsd, buf, blk_addr, blk_len, Timeout);
	HAL_SD_CardStateTypeDef status = HAL_SD_CARD_SENDING;
	
	if (res == HAL_OK)
	{
		//等待传输完成
		while(status != HAL_SD_CARD_TRANSFER)   
			status = HAL_SD_GetCardState(&hsd);
		return (USBD_OK);
	}
	else
		return (USBD_FAIL);
  /* USER CODE END 7 */
}

/*返回最大支持LUN数量,无需修改*/
int8_t STORAGE_GetMaxLun_FS(void)
{
  /* USER CODE BEGIN 8 */
  return (STORAGE_LUN_NBR - 1);
  /* USER CODE END 8 */
}

4、烧录验证

烧录程序,开发板上电后,使用USB线将Windows电脑与开发板上的USB_SLAVE接口连接,连接成功后电脑上会弹出与U盘插入时一致的弹窗(第一次可能会自动安装驱动,稍微等待等待),之后就可以在文件资源管理器中找到新的可用卷

单击打开该卷便可以像操作U盘一样对开发板上插入的SD卡进行文件管理,如下图所示笔者的SD卡里还保存着之前实验所写入的信息,读者可以自行尝试写入或删除文件

读者还可以自己将本实验与
STM32CubeMX教程28 SDIO - 使用FatFs文件系统读写SD卡
实验结合来验证使用FatFs读写SD卡操作是否真正成功,笔者通过开发板上的KEY0按键删除了原本写入SD卡中的test.txt文件,复位开发板重新识别SD卡之后,在Windwos资源管理器里查看发现test.txt文件确实被删除了,具体操作如下图所示

5、常用函数

请阅读
STM32CubeMX教程28 SDIO - 使用FatFs文件系统读写SD卡
实验“7、常用函数”小节

参考资料

STM32Cube高效开发教程(高级篇)

1、禅道介绍

禅道项目管理软件是第一款国产的开源项目管理软件。它集产品管理、项目管理、质量管理、文档管理、 组织管理于一体,在数据可视化、度量、DevOps、文档资产管理和自动化测试等模块的加持下,禅道软件形成了项目流程管理、DevOps 一体化、自动化测试等三大解决方案,完整的覆盖了产品研发的全生命周期管理,并且有完善的 API 可以调用,在国内深受大量企业喜爱使用。

禅道官网目前存在四个版本:开源版、企业版、旗舰版、云禅道,对于这四个版本之间的差异,不是本文介绍的重点,感兴趣的读者可自行查阅官网介绍

https://www.zentao.net/

很多个人使用者或者中小企业,大多数选用的是开源版,开源版的好处:只有你具备开发能力,可以基于实际不同的项目管理需求,来定制系统功能。当然如果企业不差钱或者没有人手可以定制,可以花点钱购买企业版或旗舰版。

2、禅道二开定制机制

今天我们重点来介绍一下,禅道如何进行功能二开定制。

首先,禅道项目管理软件是使用自主开发的
zentaoPHP
框架开发的。
zentaoPHP
框架实现了功能强大的扩展机制、API调用机制和命令行机制。您可以使用不同的机制来实现对禅道功能的修改、扩展。也可以和其他的系统进行集成,也可以部署各种自动化脚本对禅道进行各种操作。

zentaoPHP
框架的二次开发机制文档可参考:

1. zentaoPHP框架基本使用手册:http://devel.cnezsoft.com/book/zentaophphelp.html
2. zentaoPHP框架二次开发机制:http://devel.cnezsoft.com/book/extension.html

禅道开源版项目源码:

https://github.com/easysoft/zentaopms

一、顶级目录结构:

二、www目录:

三、module目录:

module目录下面总共有30多个模块,分别对应了禅道里面的某一个功能模块。整个禅道的功能,就是由这些模块组合而成。大部分定制功能代码都是在这个模块下来完成的。

3、一键部署禅道及禅道二开定制示例

接下来,以具体示例演示,如何在禅道里面给BUG自定义字段,其他如产品需求、项目任务定制也是一样的逻辑。

3.1 本地Docker一键搭建禅道环境

docker run --name zentao -p 80:80 -p 3306:3306 -v ~/www/zentaopms:/www/zentaopms -v ~/www/mysqldata:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=123456 --restart always -d easysoft/zentao:18.3

禅道服务启动后自带MySQL数据库服务,比如进入MySQL容器内:

查看zt_bug表结构

禅道的数据库命名都比较简明扼要,从字面意思应该都可以猜出来表的用途。如果还不是很清楚的话,可以到每个表对应的模块下面的语言文件里面查找。最新版本可以在
禅道后台->二次开发->数据库
中查看相应的表介绍,或直接查看官网文档

https://www.zentao.net/book/zentaopmshelp/157/

3.2 对BUG自定义新增字段

示例:
比如原先提交BUG时,无法记录BUG是在哪个阶段被发现的,那么我们可以通过改造禅道,自定义增加一个【BUG发现阶段】字段

改造定制过程:

1、给数据库zt_bug添加新增字段【BUG发现阶段】

ALTER TABLE zt_bug ADD COLUMN stage VARCHAR(50) AFTER title;

2、修改zentaopms/module/bug/lang/zh-cn.php
如下图,新增Bug发现阶段字段,即在合适的位置插入以下代码

接着如下图,在合适的位置插入以下代码

$lang->bug->stageList['review'] = '评审阶段';
$lang->bug->stageList['dev'] = '开发自测';
$lang->bug->stageList['test'] = '测试验证';
$lang->bug->stageList['acceptance'] = '验收阶段';
$lang->bug->stageList['production'] = '生产阶段';

说明:这里定义的是用于存放对应字段select下拉选框中的数据,当在创建Bug/编辑Bug并保存后,会自动被存储到数据库中。笔者使用的禅道,语言设置的是中文,所以仅修改zh-cn.php

3、修改zentaopms/module/bug/view/create.html.php (BUG新建页面)

<div class='table-col'>
  <div class='input-group'>
      <span class='input-group-addon fix-border'><?php echo $lang->bug->stage?></span>
        <?php echo html::select('stage', $lang->bug->stageList, $stage, "class='form-control chosen'");?>
 </div>
</div>

展示效果:

4、修改zentaopms/module/bug/view/edit.html.php (BUG编辑页面)
如下图,在合适的位置插入以下代码:

js::set('stage'                 , $bug->stage);

如下图,在合适的位置插入以下代码

<tr>
<th><?php echo $lang->bug->stage;?></th>
<td><?php echo html::select('stage', $lang->bug->stageList, $bug->stage, "class='form-control'");?></td>
</tr>
<tr>

效果展示:

5、修改zentaopms/module/bug/view/view.html.php (BUG查看页面)

如下图,在合适的位置插入以下代码:

<tr>
<th><?php echo $lang->bug->stage;?></th>
<td><strong><?php echo zget($lang->bug->stageList, $bug->stage, $bug->stage);?></strong></td>
</tr>

效果展示:

6、bug导出报表中增加新增的自定义字段名称

如下,找到
$config->bug->config->exportFields
变量,增加新增的自定义字段名称,解决缺陷导出报表中没有新增字段的问题,如下


展示效果:

7、bug搜索过滤条件中增加新增的自定义字段名称

如下,找到global $lang,在其下方合适的位置新增搜索字段,即我们自定义的字段,并给字段设置可选搜索项,解决缺陷搜索中没有新增字段问题。(需要修改两处地方)

$config->bug->search['fields']['stage']          = $lang->bug->stage;

$config->bug->search['params']['stage']          = array('operator' => '=',       'control' => 'select', 'values' => $lang->bug->stageList);

效果展示:

8、bug生成报表时新增自定义字段

添加完字段后,bug的报表统计中没有该字段的信息,无法按照自定义的字段进行bug分析,如何将自定义的字段添加到BUG的统计报表中去。
首先在zh-cn.php中添加新字段报表信息,打开zh-cn.php文件,添加报表统计字段

$lang->bug->report->charts['bugsPerStage']         = '按Bug发现阶段';

紧接着,添加报表以及图表空对象

$lang->bug->report->bugsPerStage           = new stdclass();
$lang->bug->report->bugsPerStage->graph          = new stdclass();

添加图表显示字段信息

$lang->bug->report->bugsPerStage->graph->xAxisName       = '发现阶段';

其次,在module.php中新增字段统计方法, 编辑module.php信息

/**
* Get report data of bugs stage
* 根据BUG发现阶段获取报表
* @access public
* @rerturn array
*/
public function getDataOfBugsPerStage()
  {
    $datas = $this->dao->select('stage AS name, COUNT(*) AS value')->from(TABLE_BUG)->where($this->reportCondition())->groupBy('name')->orderBy('value DESC')->fetchAll('name');
    if(!$datas) return array();
    foreach($datas as $stage => $data) if(isset($this->lang->bug->stageList[$stage])) $data->name = $this->lang->bug->stageList[$stage];
    return $datas;
  }


效果展示: