0%

电控组代码维护手册

为什么需要规范的代码?

代码规范的重要性

在一个小型团队中(人数不超过三人)进行代码维护时,经常出现版本混乱、代码重复以及团队成员之间沟通不畅的问题。特别是在不同版本的项目打包压缩时,可能会出现以下问题:

  1. 业务代码与测试代码混杂:模块功能代码和测试代码未分离,容易产生代码覆盖问题,导致修改和调试时难以确认哪些内容被改动过。
  2. 版本命名不规范:版本号不一致,导致无法清楚地知道各个版本包含哪些功能或修复了哪些问题。
  3. 代码风格不统一:不同开发人员的编程风格和规范不同,可能会导致理解上的歧义,进而产生功能错误。

官方代码风格

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* @brief 底盘无力的行为状态机下,底盘模式是raw,故而设定值会直接发送到can总线上故而将设定值都设置为0
* @author RM
* @param[in] vx_set 前进的速度,设定值将直接发送到can总线上
* @param[in] vy_set 左右的速度,设定值将直接发送到can总线上
* @param[in] wz_set 旋转的速度,设定值将直接发送到can总线上
* @param[in] chassis_move_rc_to_vector 底盘数据
* @retval 返回空
*/

static void chassis_zero_force_control(fp32 *vx_can_set, fp32 *vy_can_set, fp32 *wz_can_set, chassis_move_t *chassis_move_rc_to_vector)
{
if (vx_can_set == NULL || vy_can_set == NULL || wz_can_set == NULL || chassis_move_rc_to_vector == NULL)
{
return;
}
*vx_can_set = 0.0f;
*vy_can_set = 0.0f;
*wz_can_set = 0.0f;
}

抽象代码风格

  1. 状态机不使用状态枚举,只使用数字,降低了代码的可读性。
  2. 标志位无法判断是局部变量还是全局变量,生命周期混乱,难以维护。
  3. 函数命名混乱,有时使用大小写字母、下划线等不同风格,增加了理解难度。
  4. 函数缺乏详细注释,无法清晰知道函数的功能和参数,导致在协作时容易产生误解。
  5. 抽象层次不合理,如延时环节和业务功能代码混杂,导致代码执行流程混乱。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
//自瞄模式控制
int fire_mode = 0;
static void gimbal_autoangel_control(fp32 *yaw, fp32 *pitch, gimbal_control_t *gimbal_control_set)
{
static int step;
static int delay_time;

if (yaw == NULL || pitch == NULL || gimbal_control_set == NULL){
return;
}
switch(step){
case 0:{
//自瞄检测状态
status_auto = 0;
if(Find_IS_NOT() == 1){
step = 1;
}
}break;
case 1:{
//开启自瞄
Auto_Track(yaw,pitch,gimbal_control_set);
if(Find_IS_NOT() == 0){
step = 2;
}
}break;
case 2:{
delay_time++;
Auto_Track(yaw,pitch,gimbal_control_set);
if(delay_time < 500){
if(Find_IS_NOT() == 1){
step = 1;
delay_time = 0;
}
}else{
step = 0;
delay_time = 0;
}
}break;
}
return;
}

技术债的定义和表现

技术债是指在软件开发过程中,由于快速开发或未按照最佳实践编写代码而引入的可维护性问题或质量问题。类似于“金融债”,技术债需要通过“利息”来偿还,这表现为开发效率和维护成本的增加。

存在的问题

  1. 变量命名抽象,宏定义中使用“魔法数字”,变量的依赖关系不清晰。
  2. 函数功能不明确,函数过于复杂、实现繁琐,导致单一文件内功能代码过多,难以理解和维护。
  3. 标志位滥用,指针时而检查时而不检查,条件判断有盲区。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
/************** shoot.c ***************/

// >> 函数声明

// 初始化串口与DMA,设置数据长度
void AUTO_init(void);
// 自动射击任务,处理自动射击过程
void auto_task(void const * argument);
// 自动追踪函数,控制云台的yaw和pitch角度
void Auto_Track(fp32 *yaw, fp32 *pitch, gimbal_control_t *gimbal_control_set);
// 串口中断处理函数
void USART1_IRQHandler(void);
// 串口接收函数
void UART1_receive_IDE(void);
// 检查是否未找到目标
int Find_IS_NOT(void);
// 自瞄拟合函数,用于计算目标距离与pitch角度的关系
float Fitting_function(float distance);
// 获取自动射击数据结构指针
auto_data *get_AUTOshoot_point(void);

// >> 变量

// 外部结构体指针
extern UART_HandleTypeDef huart1; // 串口1的句柄
extern DMA_HandleTypeDef hdma_usart1_rx; // 串口1的DMA的句柄
extern ext_game_robot_state_t robot_state; // 当前机器人的状态
extern ext_power_heat_data_t power_heat_data_t; // 电源和热量数据
extern RC_ctrl_t rc_ctrl; // 遥控器控制数据
extern gimbal_control_t gimbal_control; // 云台控制数据
// 接收缓冲区和数据结构
uint8_t RX_buf[RX_BUF_NUM]; // 接收数据缓冲区
autorec recd; // 自动射击数据结构
pid_type_def auto_pid; // PID控制结构
int status_auto = 0; // 自动射击状态
int flag_received = 0; // 数据接收标志位
char date_length = 0; // 接收数据的数据长度
char auto_flag = 0; // 自动射击标志位
_feedBackFrame data_sent; // 数据返回帧
// 云台角度的偏差量
float yaw_angle_bias; // 云台yaw轴角度的偏差量
float pitch_angle_bias; // 云台pitch轴角度的偏差量
float yaw_angle, pitch_angle; // 自瞄计算之后的绝对目标角度
// 云台控制标志
uint8_t flag_shoot = 0; // 云台射击标志
// 自动射击所需的常量参数
extern fp32 INS_angle[3]; // 姿态角度
#define DATE_LENGTH 12 // 数据长度
#define RX_BUF_NUM 32 // 接收缓冲区大小
// 云台角度的限制
#define YAW_ANGLE 180.0f // yaw轴角度限制
#define PITCH_ANGLE_UP 45.0f // pitch轴上限
#define PITCH_ANGLE_DOWN -45.0f // pitch轴下限
// 拟合函数的系数
#define THREE_FACTOR 0.5f
#define TWO_FACTOR 0.3f
#define ONE_FACTOR 0.1f
#define ZERO_FACTOR 0.0f
#define NOT_FIND_GOAL -999.0f // 未找到目标的返回值

如何识别和解决技术债

识别技术债

  1. 代码审查:主要问题包括变量命名不清晰、代码块复用率低、复杂逻辑缺乏注释等。
  2. 性能瓶颈:如通信接口的数据性能瓶颈,任务执行间隔的时间片瓶颈等。

解决技术债

  1. 定期清理:定期回顾和整理已完成的功能代码,及时发现和解决存在的问题,减少后续开发中的重构和调试工作量。
  2. 重构代码:在对模块功能有深入理解后,对代码进行重构,优化代码结构,提升可维护性。

代码规范与标准

命名规范

  • 变量命名
    • 使用有意义、简洁的名称,避免使用单字母或循环字母命名,如:aaa、test1等。
    • 使用有意义的前缀来表明变量的作用域,如:g_data_length。
    • 局部变量可不使用前缀,除非函数特别长。
    • 框架变量要遵循

框架的命名规范,如:QueueHandle_t xQueue。

  • 函数命名

    • 使用小写字母和下划线分隔单词,如:shoot_feedback_update。
    • 函数名应采用动词 + 名词的组合形式,如:initialize_IMU。
    • 动词命名要统一,避免多义性。
      • initialize_*:用于初始化模块。
      • configure_*:用于配置模块。
      • set_*:用于设置参数或状态。
      • get_*:用于获取状态或值。
      • enable_ / disable_:用于启用或禁用功能。
    • 命名应简洁明了。
      • clear_buffer(清空缓冲区)
      • update_display(更新显示)
  • 常量命名

    • 宏定义和常量全大写,单词之间使用下划线分隔,如:MOTOR_RPM_TO_SPEED。
  • 结构体、枚举命名

    • 使用小写单词和下划线拼接,并在末尾加上“e”(表示枚举)或“t”(表示typedef struct),如:shoot_mode_e、gimbal_motor_t。

注释规范

  • 函数注释

    • 每个函数都应有注释,说明函数目的、输入输出参数及返回值(如果有),并备注需要注意的地方。
      1
      2
      3
      4
      5
      6
      /**
      * @brief 返回yaw电机数据指针
      * @param[in] none
      * @retval yaw电机指针
      */
      extern const gimbal_motor_t *get_yaw_motor_point(void);
  • 代码段注释

    • 对于复杂或不易理解的代码段,添加必要注释,简要介绍其功能或目的。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      HAL_UART_DMAStop(&huart1);  // 停止DMA传输
      date_length = RX_BUF_NUM - __HAL_DMA_GET_COUNTER(&hdma_usart1_rx); // 计算已接收数据
      if(date_length == DATE_LENGTH) // 数据接收长度符合设定
      {
      memcpy(recd.buf, RX_buf, DATE_LENGTH); // 复制数据至结构体
      flag_received = 1;
      }
      memset(RX_buf, 0, RX_BUF_NUM); // 清空接收缓冲区
      HAL_UART_Receive_DMA(&huart1,RX_buf, RX_BUF_NUM); // 继续接收数据
  • 条件编译指令

    • 对于灵活挂载的模块,使用条件编译,并在预编译指令旁写上对应的功能说明。
      1
      2
      3
      4
      5
      #if	(CAP_BOARD==1)  // 超级电容板使用的CAN
      // 逻辑代码
      #else // C板使用的CAN
      // 逻辑代码
      #endif

文档规范

  • 功能文档:提供系统的整体功能说明和功能模块的执行流程。

    参考开源项目介绍页面,或其他团队的开源框架。

  • 模块接口文档:详细描述每个模块的输入输出参数以及与其他模块的交互。

    参考接口文件(.c/.h)和接口文档的编写方式。

  • 硬件接口文档:描述硬件平台接口,包括GPIO配置、ADC、PWM设置等。

    参考相关开发手册和硬件教程。

版本控制与分支管理

基本操作

  • 创建分支

    • 开发新功能或者修复 Bug 时,应先创建一个新的分支,而不是直接在 main 分支上修改。
      1
      git checkout -b feature/your-feature-name
    • 例如,开发一个新的电控模块,可以创建 feature/control-module 分支。
  • 提交修改

    • 在分支上完成开发后,进行本地提交。每次提交时,确保提交信息简洁明了。
      1
      2
      git add .
      git commit -m "添加电控模块,完成基本功能"
  • 推送到远程仓库

    • 提交完代码后,将本地分支推送到远程仓库,便于其他人查看和协作。
      1
      git push origin feature/your-feature-name
  • 合并分支

    • 开发完成后,将分支合并回 main 分支,合并之前确保已经将 main 分支上的最新更新合并到当前分支。
      1
      2
      3
      4
      git checkout main
      git pull origin main # 拉取最新的 main 分支代码
      git checkout feature/your-feature-name
      git merge main # 合并 main 分支上的最新内容
  • 解决冲突

    • 合并分支时可能会出现代码冲突,Git 会标记出冲突的部分,开发者需要手动解决冲突。
      1
      2
      3
      # 在冲突文件中,手动解决冲突后
      git add <conflicted-file> # 标记冲突已解决
      git commit -m "解决分支合并冲突"
  • 删除分支

    • 合并完成后,删除已完成开发的分支,保持分支结构简洁。
      1
      2
      git branch -d feature/your-feature-name  # 删除本地分支
      git push origin --delete feature/your-feature-name # 删除远程分支

分支管理 GitHub Flow

  • 主要分支

    • main:主分支,始终保持可部署状态。
  • 功能分支

    • 开发者从 main 分支创建功能分支进行开发,开发完成后通过 pull request 进行代码审查和合并。
      1
      2
      git checkout main
      git checkout -b feature/feature-name
  • 合并到主分支

    • 提交代码后,通过 GitHub 的 Pull Request(PR)进行代码审查,确认无误后合并回 main 分支。
      1
      git push origin feature/feature-name

如何避免常见的 Git 问题

  • 避免在 main 分支上直接开发

    • 永远不要在 main 分支上直接进行功能开发或修复。开发工作应该始终在独立的功能分支上进行,确保 main 分支始终是稳定的。
  • 频繁拉取远程代码

    • 在进行任何开发之前,先从远程仓库拉取最新的代码,避免本地和远程代码差异过大。
      1
      git pull origin main
  • 编写清晰的提交信息

    • 每次提交时,都应写明提交的目的和内容。避免使用类似 “修复bug” 这样的模糊描述。
      1
      git commit -m "修复电控模块初始化时的内存问题"
  • 合并时解决冲突

    • 如果合并时遇到冲突,手动解决冲突并确保冲突解决后测试代码是否正常运行。然后再进行提交。

示例流程

假设你正在开发一个新的功能,以下是完整的 Git 操作流程:

  1. main 分支创建功能分支

    1
    2
    3
    git checkout main
    git pull origin main # 拉取最新的 main 分支
    git checkout -b feature/add-control-module
  2. 在功能分支上进行开发和提交

    • 开发功能时,频繁提交并推送到远程:
      1
      2
      3
      git add .
      git commit -m "添加电控模块基础功能"
      git push origin feature/add-control-module
  3. 合并功能分支到 main

    • 功能完成后,首先拉取 main 分支的最新代码:

      1
      2
      3
      4
      git checkout main
      git pull origin main
      git checkout feature/add-control-module
      git merge main
    • 解决合并冲突并提交:

      1
      2
      git add <resolved-file>
      git commit -m "解决合并冲突"
  4. 推送功能分支并创建 Pull Request

    • 将功能分支推送到远程并创建 Pull Request:
      1
      git push origin feature/add-control-module
  5. 代码审查和合并

    • 通过 GitHub 创建 Pull Request,请团队成员进行代码审查,审查通过后合并到 main
  6. 删除功能分支

    • 合并后,删除本地和远程的功能分支:
      1
      2
      git branch -d feature/add-control-module  # 删除本地分支
      git push origin --delete feature/add-control-module # 删除远程分支

BMS电池管理系统设计

电池参数和特性

电特性

  • 倍率:
  • 内阻:
  • 额定容量:
  • 实际容量:
  • 开路电压:
  • 端电压:
  • 截止电压:
  • 自放电:
  • 循环寿命:
  • 日历寿命:
  • CC-CV充电:
  • 荷电状态:
  • 放电深度:
  • 健康状态:
  • 功率状态:
  • 功能状态:
  • 能量密度:
  • 功率密度:
  • 单体一致性:

热特性

  • 比热容:
  • 热导系数:
  • 热失控:

电池的要求

  • 安全:
  • 质量:
  • 寿命:
  • 性能:
  • 成本:

电池的单体封装

  • 圆柱型:
  • 软包型:
  • 硬壳型:

电池的发开历程

  • 单体电池
    • 材料:
    • 设计:
    • 封装:
  • 电池系统
    • 电池测试:
    • 管理系统:
    • 热分析及管理:
    • 机械系统:
    • 电气系统:
  • 应用
    • 匹配:
    • 标定:
    • 组装:

电池管理的主要功能和目标

  • 安全问题
    • 内短路:生产过程
    • 外短路:
    • 过充电:
    • 过放电:
    • 过载:
    • 外部加热:温控环节
  • 使用寿命
    • 循环寿命:充电/放电/时间
    • 日历寿命:日历时间/SOC/电芯温度
    • 电池不一致:生产参数/温度/SOC/机械压力
  • 电池管理系统目标
    • 延长使用寿命
    • 提升使用性能
    • 提升使用安全性
  • 电池管理系统组件
    • 硬件模块
      • 采样环节
      • 均衡控制
      • 高压检测
      • 继电器控制
    • 软件模块
      • SOC/
      • 热管理
      • 快慢充控制
      • UDS诊断
      • 上下点控制
      • 在线标定
  • 电池管理特点
    • 高低压混合
    • 模数混合
    • 参数辨识
  • 热管理功能
    • 功率特性
    • 容量特性
    • 自放电率
    • 老化特性

电池系统

  • 基本拓扑
    • 先并后串
    • 先串后并
  • 考虑因素
    • 系统成本
    • 可靠性
    • 一致性问题

渗透记录—W1R3S靶机

环境介绍

  • 虚拟机环境(VMWare17):kali
    • 网关: 192.168.217.128
  • 靶机:Ubuntu16.04
    • 网关: 192.168.217.131

nmap扫描与分析

作为网络攻击方,我们首先要知道被攻击方也就是靶机的网络地址,才可以对其进行攻击,下面将使用kali自带的渗透工具对目标网段进行扫描:

1
2
3
4
5
6
7
8
# sudo: 提升命令的权限
# nmap: 网络扫描工具
# -sn: namp的ping扫描选项,只检测目标主机是否在线,步进行端口扫描。
# -sL: 默认参数,只进行扫描,不进行侵入式侦察
# 192.168.217.0/24: 指定了扫描的 IP 地址范围,这里表示从 192.168.217.0 到 192.168.217.255 的所有 IP 地址。/24 是子网掩码的简写,表示 256 个地址(C 类子网)
sudo nmap -sn 192.168.217.0/24
# 使用以下工具扫描也可以
sudo arp-scan -l

扫描结果如下:

image1

确定目标靶机是192.168.217.131,之后执行以下指令开启端口扫描

1
2
3
4
# -sT: TCP连接扫描
# -p-: -p指定端口 -表示0~65535所有端口
#
sudo nmap -sT -p- 192.168.217.131 -oA tcp

扫描结果如下:

image2

可以看到,暴露了四个开放的端口和对应端口运行的服务,我们使用命令将这几个端口保存为变量,方便之后调用。

1
2
3
4
5
# grep open ports.nmap: 找到文件中含有open的行。
# awk -F'/' '{print $1}': 处理grep找到的行,-F'/'表示使用/作为分隔符,{print $1}表示打印出每行的第一个字段。
# paste -sd ',': -s表示将多行合并为一行,-d表示使用','作为分隔符。
# ports=$(): 将输出结果赋值给一个变量。
ports=$(grep open ports.nmap | awk -F'/' '{print $1}' | paste -sd ',')

之后再使用UDP扫描一遍,就是将命令中的-sT改为-sU,这里扫描过一遍之后没有暴露出开放的端口,所以忽略。

确定开放的端口之后,就需要对开放的端口进行版本服务和漏洞的扫描了,

1
2
3
4
5
6
7
8
# -sT: TCP连接扫描
# -sV: 服务版本探测
# -sC: 使用Nmap的默认脚本进行漏洞探测
# -O: 操作系统检测
# -p$ports: 指定要扫描的端口范围,$ports是一个变量,为22,80,443这样的格式
# 192.168.217.131: 目标 IP 地址。
# -oA ports: 将扫描结果输出到文件。
sudo nmap -sT -sV -sC -O -p$ports 192.168.217.131 -oA ports

扫描结果如下:

image3

可以看到对于暴露的四个端口,扫描出了对应的服务和系统的版本。

  • 21端口: 使用的FTP服务,版本号:vsftp 2.0.8,允许匿名FTP登录,并且列出了一些文件和文件夹权限。
  • 22端口: ssh服务端口,版本号:7.2p2,显示了支持的SSH密钥格式及其指纹信息。
  • 80端口: http服务,版本号:Apache HTTPD 2.4.18,放的是默认页面。
  • 3306端口: mysql服务,显示未经授权不能访问,没有更多信息。
  • 系统版本: Linux系统

首先我们观察一下以上四个攻击向量,相对来说比较容易的突破口就是21端口的FTP服务,这个允许匿名登陆,我们就可以先匿名登录看一下FTP服务器上的内容,执行以下指令访问远程FTP服务。

1
2
# 使用FTP服务访问远程服务器
ftp 192.168.217.131

WSL2(Ubuntu 24.04)上开发RT-Thread操作系统

安装环境

  • RT-Thread源码

    1
    2
    3
    git clone https://github.com/RT-Thread/rt-thread.git
    # 国内镜像
    git clone https://gitee.com/RT-Thread/rt-thread.git
  • 安装QEMU,这是一个仿真器,用来模拟RT-Thread在ARM架构上运行

    1
    sudo apt-get install qemu-system-arm
  • 安装python和scons环境(RT-Thread的qemu配置脚本是使用scons写的,需要在python环境中安装scons和kconfiglib)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    sudo apt install python3.12
    # 切换到源码根目录
    cd rt-thread/
    # 创建python虚拟环境
    python3 -m venv .rt_thread_env
    # 激活虚拟环境
    source .rt_thread_env/bin/activate
    # 安装scons和kconfiglib(前置环境)
    pip3 install scons kconfiglib
  • 安装交叉编译器(因为编译RT-Thread要运行在ARM芯片上,所以需要使用gcc-arm的交叉编译环境),版本号要高于:6.2.1

    1
    2
    3
    sudo apt install -y gcc-arm-none-eabi
    # 查看版本号
    arm-none-eabi-gcc --version

    版本号: arm-none-eabi-gcc (15:13.2.rel1-2) 13.2.1 20231009

  • 安装ncuses库

    1
    sudo apt-get install libncurses5-dev

RT-Thread下的qemu环境配置

进入RT-Thread的根目录,之后执行以下指令:

1
2
3
4
5
6
7
8
9
10
# 进入模拟器环境
cd /bsp/qemu-vexpress-a9
# 进入配置环境,选择需要的配置
scons --menuconfig
# 编译源码
scons
# 授予运行权限
chmod +x ./qemu.sh
# 运行模拟器
sudo ./qemu.sh

项目代码编写

进入/applications/main.c文件,编写项目代码,实现需要实现的功能,比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/*
* Copyright (c) 2006-2020, RT-Thread Development Team
*
* SPDX-License-Identifier: Apache-2.0
*
* Change Logs:
* Date Author Notes
* 2020/12/31 Bernard Add license info
*/

#include <stdint.h>
#include <stdio.h>
#include <rtthread.h>
#include <stdbool.h>

void thread_entry(void *parameter){
while(true){
rt_kprintf("Thread is running.\n");
rt_thread_delay(1000);
}
}

int main(void)
{
rt_kprintf("Hello, This is Javen.\n");
//创建线程
rt_thread_t thread = rt_thread_create("my_thread", thread_entry, RT_NULL, 1024, 10, 20);
if(thread != RT_NULL){
rt_thread_startup(thread);
}

return 0;
}

修改结束之后需要重新编译

参考链接

  1. FinSH控制台组件
  2. 在Ubuntu平台上开发RT-Thread
  3. 在windows平台使用qemu-vexpress-a9

矩阵求导

标量方程对向量求导

在现代控制理论中,很多理论的推导都需要使用到矩阵的运算,尤其是在计算最优控制中,会经常需要求解多维方程的导数。

基本定义

对于一个标量函数,其中维列向量,标量对向量的求导得到的是梯度向量。

  • 梯度向量:

这意味着对每一个分量都分别求偏导。

常见的标量函数对向量的求导

线性函数

其中,是一个常向量。

  • 求导结果:

二次型函数

其中,是一个常向量。

  • 求导结果:

欧几里得范数平方

  • 求导结果:

二范数函数

  • 求导结果:

链式法则

如果标量函数是通过一个向量函数间接定义的,即,那么我们需要使用链式法则

例子:求解一个具体的标量函数的梯度

  • 损失函数为:

    其中,

  • 求梯度:

  • 求导:

Pytorch框架学习—图片分类

导入必要的库和模块

1
2
3
4
import torch # PyTorchd的核心库,提供张量(Tensor)计算和自动微分等功能
from torchvision import models, transforms # PyTorch的计算机视觉库,包括了预训练模型、图像处理工具和常用数据集
from torchvision.models import ResNet101_Weights # 包括了用到的模型的预训练权重和类别标签
from PIL import Image # 加载图片模块

加载与训练的ResNet101模型

1
2
3
# models.resnet101:ResNet-101模型,该网络有101层,是一种深度残差网络
# weights=ResNet101_Weights.IMAGENET1K_V1:表示使用的与训练的权重
resnet = models.resnet101(weights=ResNet101_Weights.IMAGENET1K_V1)

image1

深度残差网络(ResNet-101)使用“残差块”,通过引入快捷连接解决深层网络中的梯度消失问题。ResNet-101有超过百万个参数,适合处理复杂的图像特征。

图像预处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# transforms.Compose:将多个图像处理操作组合成一个顺序管道。
# Resize(256):将图片大小调整为256像素的边长。
# CenterCrop(224):将图片中心裁剪成224×224像素,这是 ResNet 模型输入的标准尺寸。
# ToTensor():将图片数据转换为张量,并将像素值归一化到 [0, 1] 范围内。
# Normalize(mean, std):标准化处理,调整颜色分布使其符合ImageNet上的训练数据。将每个通道的均值减去并除以标准差。
preprocess = transforms.Compose([
transforms.Resize(256),
transforms.CenterCrop(224),
transforms.ToTensor(),
transforms.Normalize(
mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225]
)
])

深度学习模型对于输入的尺度比较敏感,因此通常需要对输入的数据进行标准化处理。这样可以保证输入分布与训练数据一致,从而提高模型的泛化能力。

加载和预处理图像

1
2
3
4
5
6
# Image.open:打开图片文件。
# preprocess(img):对图像执行上一步定义的预处理操作。
# torch.unsqueeze(img_t, 0):为模型输入添加一个批次维度,使图像数据形状符合模型输入的要求(1, 3, 224, 224)。
img = Image.open("./Resnet/Image/image1.jpg")
img_t = preprocess(img)
batch_t = torch.unsqueeze(img_t, 0)

image2

卷积神经网络的期待输入的图片张量形状为(batch_szie,channels,height,weight),在这里,unsqueeze在第0维度增加了batch大小。

设置模型的模式

1
2
# eval():将模型切换到评估模式,冻结 dropout 和 batch normalization 等层的行为,以确保一致性。
resnet.eval()

image3

在训练和评估时,某些层(如 dropout 和 batch normalization)会有不同的行为,eval() 方法确保它们在预测阶段保持一致。

获取模型输出并计算预测类别

1
2
3
4
# resnet(batch_t):将预处理后的图像张量传入模型,得到输出向量 out。
# torch.max(out, 1):沿输出的每行(类别维度)找到概率最大的值,其索引即为预测类别的下标 index。
out = resnet(batch_t)
_, index = torch.max(out, 1)

image4

ResNet 输出的是一个长度为1000的张量,每个值对应一个类别的“未归一化”概率。torch.max 用于提取模型认为最可能的类别。

使用Softmax获得类别的百分比

1
2
3
# softmax:将模型输出的各类别值转换为概率,使它们的总和为1,得到各类别的相对可能性。
# [0] * 100:提取该图像的所有类别的百分比值。
percentage = torch.nn.functional.softmax(out, dim=1)[0] * 100

Softmax 层将向量值转化为概率分布,用于多分类问题的输出,确保总概率为100%。

获取ImageNet标签并打印模型预测的标签和百分比

1
2
3
4
5
6
# ResNet101_Weights.IMAGENET1K_V1.meta["categories"]:获取 ImageNet 数据集的类别标签。
# predicted_label:通过 index 索引找到预测类别的标签。
# print(...):打印出预测的标签和对应的概率百分比。
labels = ResNet101_Weights.IMAGENET1K_V1.meta["categories"]
predicted_label = labels[index[0]]
print(f"Predicted label: {predicted_label}, Confidence: {percentage[index[0]].item():.2f}%")

image5

ResNet101_Weights.IMAGENET1K_V1 中的 meta 提供类别标签字典,模型输出的类别索引对应到实际的标签名称。

概率分布前五的label

image6

完整代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import torch
from torchvision import models, transforms
from torchvision.models import ResNet101_Weights
from PIL import Image

# 加载ResNet模型
resnet = models.resnet101(weights=ResNet101_Weights.IMAGENET1K_V1)

# 图像预处理
preprocess = transforms.Compose([
transforms.Resize(256),
transforms.CenterCrop(224),
transforms.ToTensor(),
transforms.Normalize(
mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225]
)
])

# 加载和预处理图片
img = Image.open("./Resnet/Image/image10.jpg")
img_t = preprocess(img)
batch_t = torch.unsqueeze(img_t, 0)

# 设置模型为评估模式
resnet.eval()

# 获取模型输出
out = resnet(batch_t)
_, index = torch.max(out, 1)

# 使用Softmax获得类别的百分比
percentage = torch.nn.functional.softmax(out, dim=1)[0] * 100

# 获取ImageNet标签
labels = ResNet101_Weights.IMAGENET1K_V1.meta["categories"]

# 打印模型预测的标签和百分比
predicted_label = labels[index[0]]
print(f"Predicted label: {predicted_label}, Confidence: {percentage[index[0]].item():.2f}%")

Jupyter脚本文件下载

Lyapunov稳定性分析

李雅普诺夫稳定性

李雅普诺夫稳定是一种用于判断动态系统平衡点稳定性的理论,它描述了当系统从平衡状态收到微小扰动时,系统的状态如何变化。

稳定性分析

对于一个状态方程,如果系统的平衡点是,那么该点的李雅普诺夫稳定性可以从以下几个方面进行讨论:

  1. 李雅普诺夫稳定性(Lyapunov Stability)

    如果系统从平衡点受到一个微小扰动之后,系统状态始终停留在的某个小邻域内,则称平衡点是李雅普诺夫稳定的。

  2. 渐近稳定(Asymptotically Stable)

    如果平衡点是李雅普诺夫稳定的,并且系统状态随时间趋向于,即,则称平衡点是渐近稳定的。

  3. 全局渐进稳定(Globally Asymptotically Stable)

    如果无论初始状态在哪里,系统的状态最终都会趋于平衡点,则称该平衡点是全局渐进稳定的。

稳定性判断

李雅普诺夫稳定性分析可以通过李雅普诺夫函数(Lyapunov Function)。它是一种类似于能量函数的构造,用来研究系统状态的变化趋势。

李雅普诺夫函数的构造

一个函数被称为李雅普诺夫函数,需要满足以下条件:

  1. 正定性(Positive Definite):

    (即:函数在平衡点外为正,平衡点处为零)

  2. 函数的负定导数(Negative Definite Derivative)
    计算李雅普诺夫函数沿着系统轨迹的导数:

    如果处,说明系统的状态会趋向于平衡点。

  3. 渐进稳定的条件:
    对所有的成立,则平衡点是渐进稳定的。

  4. 全局渐进稳定的条件:
    如果李雅普诺夫函数在整个状态空间内都是正定的,并且其导数也是负定的,则该点是全局渐进稳定的。

MATLAB代码实现

1
2
3
4
5
6
7
8
9
10
11
12
% 定义系统向量场
[x1, x2] = meshgrid(-2:0.5:2, -2:0.5:2);
% 定义状态方程
dx1 = -x2;
dx2 = x1;

% 绘制相平面
figure;
quiver(x1, x2, dx1, dx2, 'r');
xlabel('x_1'); ylabel('x_2');
title('稳定性分析');
axis equal;

其中的定义状态方程可以换成需要设置的对应的状态方程,下面给出一些典型的用于稳定性分析的状态方程的公式和图像分析:

  • 李雅普诺夫稳定但不渐近稳定的系统

    image1

  • 渐进稳定系统

    image1

  • 非线性全局渐进稳定系统

    image1

  • 不稳定系统

    image1

图片绘制工具:MATLAB R2022a

LQR控制器的设计与推导

LQR解决的是什么问题?

对于一个离散时间的线性系统,系统的状态方程为:

其中:

  • 𝕟是第步时系统的状态;
  • 𝕞是第步时的控制输入;
  • 𝕟𝕟是状态转移矩阵;
  • 𝕟𝕞是控制矩阵。

LQR的目的是设计控制输入,使得以下的性能指标(代价函数)最小化:

其中:

  • 𝕟𝕟是半正定矩阵(即),表示状态相关的代价;
  • 𝕞𝕞是半正定矩阵(即),表示控制输入相关的代价。

基本思路

LQR问题的目标是最小化系统在状态空间和输入空间中的能量损耗。其核心思想是通过动态规划和Ballman最优化原理,找到能够最小化上述性能指标的的最优控制律

倒推求解Bellman方程

首先,我们从动态规划的思想入手,从最终状态往回推导。假设在第步时,定义代价函数的形式为:

(为什么长这个样子呢,是因为我们设计的代价函数就是考虑当前状态和输入的一个二次型代价函数,在某一时刻,两项合并就是一个二次型代价函数)

其中是某一个待求解的对称正定矩阵(或者半正定矩阵)。

为了使代价函数最优,控制律应该使得最小。因此,对于每一步,我们都要解一个最优化问题。

步时的代价

在第步时,代价函数为:

假设系统的状态方程,可以将写为:

展开后得:

递推代价函数

代价函数的表达式为:(本次的代价+之后的所有代价)

即:

将所有与有关的项提取出来,得到:

最优控制律

为了最小化代价函数,我们对求导并令其为零:

解的最优控制律为:

即:

其中是反馈增益矩阵。

Riccati方程

将最优控制律代入代价函数中,得到的递推公式:

这就是离散时间黎卡笛方程。在求解LQR问题时,我们通过迭代该方程来找到最优矩阵,从而进一步得到最优控制律。

稳定状态下的解

当系统达到稳定状态时,矩阵收敛到一个稳定值,此时黎卡笛方程变成:

通过求解这个方程,我们可以得到举证,进而得到最优控制律:

其中

整体流程

  1. 定义系统的状态方程和性能指标;
  2. 使用动态规划,从Bellman方程出发,递归得到最优控制律;
  3. 通过求解离散Ricatti方程,得到反馈矩阵
  4. 在稳定状态下,控制律为

参考书籍和文献

  1. Anderson, B. D. O., & Moore, J. B. (2007).
    Optimal Control: Linear Quadratic Methods. Dover Publications.
  2. 《自动控制原理(第七版)》胡寿松
  3. 【最优控制】5_线性二次型调节器(LQR)详细数学推导

现代控制理论—连续系统的离散化

连续系统和离散系统

  • 连续系统:时间是连续变量,状态方程和输出方程在连续时间上定义,常见形式为微分方程。
    • 状态方程:
  • 离散系统:时间是离散变量,只在特定的采样时刻(为整数,为采样周期)进行更新,常用差分形式描述。
    • 离散状态方程:

连续系统离散化的常见方法

矩阵指数法(Exact Discretization)

  • 假设输入在一个采样周期内保持恒定(也就是在每一个输出周期内,持续保持一个输出值,直到下一个输出周期到达),利用矩阵指数计算状态转移矩阵:
  • 这种方法精确且适合于线性系统,但计算矩阵指数需要较多计算资源。

零阶保持(ZOH,Zero-Order Hold)

  • 假设在每个采样周期,输入保持恒定。这是数字控制器中最常见的假设。
  • 离散化结果:
  • 优点:零阶保持方法真是反映了输入的恒定值,适用于数字控制系统。
  • 缺点:仅适用于输入恒定的情况,无法捕捉输入频繁变化的场景。

向后差分器(Backward Difference Method)

  • 用差分近似替代连续系统的微分:
  • 得到离散状态方程:
  • 优点:该方法计算简单。
  • 缺点:在系统动态较快时,误差较大。

双线性变换(Tustin变换)

  • 双线性变换通过将-域中的系统映射到-域:
  • 离散化之后的传递函数:
  • 优点:保留了系统的稳定性,适用于模拟动态快的系统。
  • 缺点:对于高频成分的处理不够精确。

连续系统离散化的关键问题

  • 采样周期的选择:采样周期太短会导致计算成本的增加,太长会丢失系统的动态信息。通常,采样周期应满足奈奎斯特采样定律,即采样频率大于信号最高频率的两倍。
  • 量化误差:离散化过程中需要考虑计算机处理中的精度限制,避免由于量化误差导致的系统不稳定性。
  • 抗混叠滤波:在进行离散化之前,常需要对连续信号进行滤波处理,以避免高频成分造成混叠现象。

MATLAB实现示例

以下是MATLAB中实现连续系统到离散系统转换的代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
% 定义连续系统状态空间模型
A = [0 1; -2 -3];
B = [0; 1];
C = [1 0];
D = 0;

% 采样周期
T = 0.1;

% 使用零阶保持法进行离散化
sys_c = ss(A, B, C, D); % 连续系统
sys_d = c2d(sys_c, T, 'zoh'); % 离散系统

% 输出离散化后的系统矩阵
[A_d, B_d, C_d, D_d] = ssdata(sys_d);
disp('离散化后的系统矩阵:');
disp('A_d = '); disp(A_d);
disp('B_d = '); disp(B_d);
disp('C_d = '); disp(C_d);
disp('D_d = '); disp(D_d);