2023年4月


大家好,我是痞子衡,是正经搞技术的痞子。今天痞子衡给大家介绍的是
利用i.MXRT1xxx系列ROM集成的DCD功能可轻松配置指定外设

关于 i.MXRT1xxx 系列芯片 BootROM 中集成的 DCD 功能这个话题,痞子衡早就想写了,但是一直没有动笔,毕竟这个话题比较生涩,单独讲会比较枯燥。最近痞子衡在支持一个 i.MXRT1170 客户,需要在客户板卡上跑其应用代码的压力测试,但是客户因为保密的缘故仅提供了应用可执行文件,而我们又需要在客户应用里额外加一些配置代码做测试,测试过程中会涉及多次断电上电,如果挂外部调试器去做额外配置又太繁琐,这时候 DCD 功能就派上用场了。

  • Note:文中贴图、代码主要以 i.MXRT1170 为例,其余 i.MXRT1xxx 系列原理类似。

一、DCD是什么及其应用场景

DCD 是 Device Configuration Data 缩写,这是 i.MXRT1xxx 系列芯片 BootROM 里带的一个附加功能,主要用于 App 启动前系统外设的用户定制化配置。我们知道 i.MXRT1xxx 系列芯片上电永远都是 BootROM 代码先执行,然后由 BootROM 再去加载 App 执行。如果希望在 App 执行前系统就已经被配置到指定状态(即不需要在 App 里去做这方面系统设置),那就需要借助 DCD 功能,你只需要按格式将 DCD 数据放到 Boot Device 指定偏移处即可,BootROM 会自动去解析执行。

翻看芯片参考手册
Device Configuration Data (DCD)
章节,你会发现 DCD 数据设计特别简单,它总共支持三类命令: Write data(Tag 是 0xCC)、Check data(Tag 是 0xCF)、NOP(Tag 是 0xC0),这三类命令就是为了读写芯片外设寄存器而设计的,我们需要做的就是组合这三类命令完成指定外设模块寄存器的设置序列。任意打开一个 RT1170 SDK 示例工程,都会包含 dcd.c/h 文件(仅当工程选项预编译宏里有 XIP_BOOT_HEADER_DCD_ENABLE=1 才会被使能)。

随便摘其中两句分析下,第一句表明是 Write data 命令的 *address = val_msk 动作合集,第二句是执行 *((uint32_t *)0x40CC0200) = 0x00000703,也就是 CCM->CLOCK_ROOT[kCLOCK_Root_Semc].CONTROL = 0x703。

/* #1.1-129, command header bytes for merged 'Write - value' command */
0xCC, 0x04, 0x0C, 0x04,
/* #1.1, command: write_value, address: CCM_CLOCK_ROOT4_CONTROL, value: 0x703, size: 4 */
0x40, 0xCC, 0x02, 0x00, 0x00, 0x00, 0x07, 0x03,

接着这个示例 dcd.c 内容继续聊,这其实是配置芯片 SEMC 外设去初始化外部 SDRAM 的全部序列。有了这个 DCD 设置,那么 App 里就可以不用管外部 SDRAM 初始化工作了,直接读写访问 SDRAM 完成相应应用业务功能即可,这也是 DCD 的典型应用场景。如果应用代码直接是全部在 SDRAM 执行,在不设计用户二级 Bootloader 做加载的情况下,DCD 是必选的解决方案。

二、以实际客户案例代入DCD使用

现在回到客户的实际案例,客户 RT1170 板卡上用了一颗来自 MXIC 的 Octal Flash,代码是执行在 Flash 上,现在我们需要测试不同 FlexSPI1->DLLACR[SLVDLYTARGET] 设置下的工作情况,而我们手头仅有客户可执行文件。

将客户可执行文件下载进板卡,并设置启动模式为从 Flash 启动(2'b10),然后挂上 JLINK 调试器读取 FlexSPI1->DLLACR 寄存器值(该寄存器地址是 0x400cc0c0),得到 0x00400079,其中 SLVDLYTARGET 是默认的理想值 4'b6666661,这个值是 BootROM 自动配置的,我们无法通过 FDCB 启动头来更改设置。

为了做压力测试,我们需要更改不同的 FlexSPI1->DLLACR[SLVDLYTARGET] 值,比如将其设为 4'b1000,这时候可以借助 DCD 来实现,我们直接使用 MCUBootUtility 工具(需要使用 v4.1.1 版本及以上)来使能 DCD。

将客户板卡启动模式改为 Serial Download (2'b01),插上 UART/USB 下载线,打开 MCUBootUtility 工具,在 DCD 设置界面里启用 "Use DCD description" 选项,并在动作框里直接输入下面语句(这里直接是类 C 语法,会被工具自动转成 DCD 数据),然后连接、下载。

*(uint32_t*)0x400cc0c0 = 0x00400041;

将板卡设为从 Flash 启动模式后重新上电,挂上 JLINK 再去读取,此时 FlexSPI1->DLLACR 已经是期望的 0x00400041,说明 DCD 功能生效了。这里还有一个注意事项,即 BootROM 利用 FDCB 启动头配置 FlexSPI 外设在前,解析执行 DCD 数据在后,所以我们才能借助 DCD 实现这样的更改测试。

三、DCD能配置全部外设吗?

看起来 DCD 特别强大,那么它能帮助操作 ARM 4GB 系统空间里的全部地址吗?答案是否定的,出于安全考虑,BootROM 里做了地址限制,我们仅能用 DCD 操作如下指定的一些外设(不同 i.MXRT 系列有所不同):

至此,利用i.MXRT1xxx系列ROM集成的DCD功能可轻松配置指定外设痞子衡便介绍完毕了,掌声在哪里~~~

欢迎订阅

文章会同时发布到我的
博客园主页

CSDN主页

知乎主页

微信公众号
平台上。

微信搜索"
痞子衡嵌入式
"或者扫描下面二维码,就可以在手机上第一时间看了哦。

ZY.Node.Mongodb

项目介绍


  • 基于 Node.js、Express.js 和 MongoDB 通过Mongoose驱动进行 REST API 开发的轻量级样板。
    集成了Swagger UI、JWT、session、发送邮箱验证、日志管理、统一的预定义状态码响应格式等,对于为前端平台构建纯净的Web API非常有用。

  • 该项目我尝试做出轻松维护代码结构的项目样板,因为任何初学者也可以采用该流程并开始构建API。

  • 项目开放,可以提出建议、错误的issues。

项目特点


  • 轻量级Node.js项目提供Restful API
  • 数据库采用 Mongodb, 通过Mongoose驱动。
  • CRUD操作示例
  • 跨域处理
  • 日志管理
  • 具有恰当的状态代码的预定义响应结构
  • 全局错误处理
  • 增加express-validator请求参数校验
  • jwt验证 用户权限中间件分离
  • 基本身份验证(采用bcrypt单向Hash加密算法加密密码进行注册/登录)
  • Token生成和校验请求头的authorization
  • 集成swagger-ui
  • 增加邮件验证码通知
  • session 验证码校验
  • 采用jest 接口单元测试

如何获得并运行项目:


首先确保您系统中安装了
Mongodb
,和
Nodejs
,一起准备完善之后。按照如下操作。
有的同学启动会报错可能是依赖包的版本问题需要注意
我的环境配置供参考

  • Node.js 14.18.1+
  • MongoDB 5.1+

1,clone代码

git clone https://gitee.com/Z568_568/node.mongodb.git

2,安装依赖

npm install

3,启动程序

开发环境:npm run dev 基于 nodemon 热更新
生产环境:npm run start

4,启动成功示例

项目启动成功

如何增加新的model


只需在
/models/v1/mapping/
中创建一个新文件,
然后在控制器
/controllers/v1/
中创建一个对应新模型的新控制器的并使用。

如何增加新的路由router


只需在
/routes/v1/
中创建一个新文件,并且引入它对应的controllers进行使用,routes下面的文件 将被
mount-routes
插件自动解析并以文件名作为api根路径,
开发环境下会打印在终端上:

路由列表

如何增加新的控制器


只需在
/controllers/v1/
中创建一个新文件即可,并且引入相关模型进行使用

如何理解内置的登录注册逻辑


/*
* TODO: 注册登录大概逻辑:
*       1.用户注册 - 用户信息入库 - 发送验证码 - 校验验证码
*                                   |
*                           验证码发送失败、验证码失效或者校验失败 - 重新发送或输入验证码 - 校验验证码
*   *
*       2.用户登录 -(账号、密码、验证状态都校验通过)- 发Token
*                       |
*                 验证状态不通过 - 重新校验验证码/重发验证码 - 校验验证码 - 发Token
* */

项目结构


.
├── app.js                  //入口文件
├── package.json            //依赖配置文件
├── .env.development        //开发环境配置
├── .env.production         //生产环境配置
├── config                  //项目配置
│   ├── db.config.js
│   ├── swagger.config.js
│   └── ...
├── controllers             //控制模块(业务处理)
│   └── v1
│       ├── UserController.js
│       └── ...
├── models                  //模型模块(建表)
│   └── v1
│       ├── user.test.js        /模型统一导出
│       └── mapping
│           ├──UserModel.js
│           └── ...
├── routes                  //路由(配置实际API地址路径)
│     └── v1
│         ├── user.test.js
│         ├── user.js
│         └── ...
├── db                      //mongodb数据库连接
│   ├── user.test.js              
│   └── ...
├── middlewares             //中间件
│   ├── jwt.js
│   ├── permissions.js
│   ├── session.js
│   └── ...
├── logs                    //日志
│   ├── info.log
│   ├── error.log
│   └── ...
└── utils                   //辅助工具
    ├── utils.apiResponse.js
    ├── utils.mailer.js.js
    └── ...

关于作者


创建和维护由

Python类

Python类的设计原则

  • 封装(Encapsulation):Python类被设计用来将相关数据和行为封装到一个独立的单元中。
  • 继承(Inheritance):Python支持继承,允许子类从父类继承属性和方法。有利于代码的复用和创建相关类的层次结构。
  • 多态(Polymorphism):Python支持多态,也就意味着不同类的对象可以被视为同一类型,这使得代码的设计具有更大的灵活性。
  • 组合(Composition):Python鼓励使用组合而不是继承,这意味着对象是由其他对象组成的,而不是由父类派生的。这可以使您的代码更加模块化,更易于维护。
  • Duck类型(Duck typing):Python使用Duck类型,这意味着对象的类型由其行为而非类决定,这样更容易编写可用于各种对象的通用代码。
  • 特殊方法(Special methods):Python提供了许多特殊方法,允许为内置操作自定义行为。例如
    __len__
    方法允许你定义
    len()
    函数如何处理类的对象。
  • 可读性(Readability):与Python的一般设计原则一样,类也应该优先考虑可读性和简单性,也就是说,对方法和属性使用清晰简洁的名称,避免不必要的复杂性。
特殊方法【Special methods】

Python中的特殊方法是一系列预定义的方法,允许你为对象上的内置操作定义自定义行为,使用双下划线前缀和后缀来识别,也成为"dunder"方法。这里讲述一些Python类中常用的特殊方法:

  1. __init__(self, ...)
    : 这是构造方法,对象创建时调用,用于初始化对象的属性并设置其初始状态。
  2. __str__(self)
    : 返回对象的字符串表示,用于创建一个人类可读的字符串表示。
  3. __repr__(self)
    : 返回对象的字符串表示,用于创建可用于创建对象的字符串表示。
  4. __len__(self)
    : 返回对象的长度,供
    len()
    函数使用。
  5. __getitem__(self, key)
    : 这个方法允许你以括号的形式访问对象的元素,可以使用
    []
    来访问元素。
  6. __setitem__(self, key, value)
    : 这个方法允许你以括号的形式设置对象元素的值,可以使用
    []
    来修改元素的值。
  7. __delitem__(self, key)
    : 这个方法允许你以括号的形式删除对象的元素。
  8. __add__(self, other)
    : 自定义对象的
    +
    操作方式。
  9. __eq__(self, other)
    : 自定义对象的
    ==
    操作方式。
  10. __lt__(self, other)
    : 自定义对象的
    <
    操作方式。

这只是较为常用的一些,Python中还提供了很多其他的特殊方法。在你的自定义类中,通过定义这些方法,你可以自定义你对象的行为以更直观、更方便地使用它们。

示例一:

class Person:
    def __init__(self, name, age, items):
        self.name = name
        self.age = age
        self.items = items
    
    def __str__(self):
        return f"{self.name} ({self.age})"
    
    def __repr__(self):
        return f"Person(name='{self.name}', age={self.age})"
    
    
person = Person("Alice", 25)
print(person)       # output: Alice (25)
print(repr(person)) # output: Person(name='Alice', age=25)

示例二:

class MyObject:
    def __init__(self, items):
        self.items = items
    
    def __len__(self):
        return len(self.items)
    
    def __getitem__(self, index):
        return self.items[index]
    
    def __setitem__(self, index, value):
        self.items[index] = value
    
    def __delitem__(self, index):
        del self.items[index]
    
    def __add__(self, other):
        new_items = self.items + other.items
        return MyObject(new_items)
    
    def __eq__(self, other):
        if isinstance(other, MyObject):
            return self.items == other.items
        return False
    
    def __lt__(self, other):
        if isinstance(other, MyObject):
            return len(self.items) < len(other.items)
        return NotImplemented
    
obj = MyObject([1, 2, 3])
print(len(obj))  # output: 3
print(obj[1])    # output: 2
obj[1] = 4
print(obj.items) # output: [1, 4, 3]
del obj[1]
print(obj.items) # output: [1, 3]
obj1 = MyObject([1, 2])
obj2 = MyObject([3, 4])
obj3 = obj1 + obj2
print(obj3.items) # output: [1, 2, 3, 4]

obj4 = MyObject([1, 2, 3])
obj5 = MyObject([4, 5])
obj6 = MyObject([1, 2, 3])

print(obj1 == obj2) # output: False
print(obj1 == obj3) # output: True

print(obj1 < obj2) # output: False
print(obj2 < obj1) # output: True
  • 格式化字符串

详细解释下这句话,字符串文字前面的
f
用于创建格式化字符串(
f-string, formatted string
),格式化字符串是一种允许你使用大括号
{}
将表达式嵌入占位符中的字符串,这些表达式将在运行时被求值并替换为它们的值。

例如下面的代码中,嵌入的表达式有两个
{self.name}

{self.age}
,运行时,会将这两个表达式替换为其值并返回。

f"Person(name='{self.name}', age={self.age})"
Duck typing

The term "duck typing" comes from the saying, "If it looks like a duck, swims like a duck, and quacks like a duck, then it probably is a duck."

Duck类型是Python等动态类型编程语言中使用的一种编程概念。在Duck类型中,一个对象的类型是由它的行为决定的,而不是它的class或者type决定的。Duck Typing一词来源于这样一句话“如果它看起来像鸭子,游泳像鸭子,嘎嘎叫像鸭子,那么它可能就是鸭子”。

Duck类型使得程序员可以编写适用于任何具有必要属性和方法的对象的代码,比如:

def print_size(obj):
    print(len(obj))

该函数接收一个参数
obj
并打印其size。由于Python使用duck类型,这个函数适用于任何包含
len()
方法的对象,比如字符串、列表、元组、字典等等(string, list, tuple, dictionaries and so on)。

使用Duck类型可以编写更加通用的代码,使得代码更加容易复用和维护。

但是使用Duck类型也极易出错,为了避免这些错误,可是使用类型检查来确保你的代码运行在你期望的对象类型上。

内置函数
  • 关于
    len(obj)
    中的
    len()

len()
是Python内置函数,用于返回对象的长度。

len()
可被用于多种类型的对象,包括字符串、列表、元组、字典、集合等,并返回对象中元素的数目。

在自定义的类中,通过实现特殊方法
__len__()
来使用
len()
函数来检索对象的长度,返回值要求是整数。

  • 关于内置函数的更多细节

    上面讲述的是其中一个内置函数,详细看下,Python中的一些其他内置函数:


    1. print()
      : 打印特定信息到控制台,对应
      __str__()
    2. type()
      : 返回对象类型,对应
      __class__()
    3. len()
      : 返回对象长度,对应
      __len__()
    4. range()
      : 生成数字序列,默认从0开始,步长为1,直到某个终止值[不包括]
    5. input()
      : 接收控制台输入
    6. int(), float(), str()
      : 将输入转换为某种特定类型,整数、浮点数或字符串。
    7. max(), min()
      : 在一个序列中找到最大值和最小值,适用于列表、元组、集合等,对应
      __gt__

      __lt__()
    8. sum()
      : 取得序列值得和,适用于列表、元组、集合等
    9. sorted()
      以升序排序一个序列,适用于列表、元组、集合等
    10. zip()
      : 将两个或多个序列合并为一个元组序列

示例一:

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def __str__(self):
        return f"Person(name='{self.name}', age={self.age})"

    def __lt__(self, other):
        return self.age < other.age

    def __gt__(self, other):
        return self.age > other.age

p1 = Person("Alice", 25)
p2 = Person("Bob", 30)
p3 = Person("Charlie", 20)

people = [p1, p2, p3]

print(p1)        # Output: Person(name='Alice', age=25)
print(min(people).name)  # Output: 'Charlie'
print(max(people).name)  # Output: 'Bob'

特殊方法与内置函数的对应:

特殊方法 内置函数 备注
__len__() len() 返回对象的长度
__getitem__(), __setitem__() [] 定义对象元素的访问和修改行为
__iter__(), __next__() for
定义对象的迭代行为
__contains__() in 判断对象是否包含某个特定元素
__add__(), __radd__() + 定义对象的加法行为
__sub__(), __rsub__() - 定义对象的减法行为
__mul__(), __rmul__() * 定义对象的乘法行为
__divmod__(), __rdivmod__() divmod() 定义对象的余数除法行为
__eq__(), __ne__() ==, != 定义对象的判等操作
__lt__(), __le__(),
__gt__(), __ge__()
<, <=, >, >= 定义对象间的比较操作
__hash__() hash() 定义对象的哈希值(hash value)

English Version

The key design principles for Python classes

  • Encapsulation:Python classes are designed to encapsulated related data and bahavior into a single unit.
  • Inheritance:Python supports inheritance, which allows classes to inherit attributes and methods from parent classes. This makes it easier to reuse code and create hierarchies of related classes.
  • Polymorphism:Python supports polymorphism, which means that objects of different classes can be treated as if they are the same type. This allows for greater flexibility in the design of your code.
  • Composition:Python encourages the use of composition over inheritance, which means that objects are made up of other objects rather than being derived from parent classes. This can make your code more modular and easier to maintain.
  • Duck typing: Python uses duck typing, which means that the type of an object is determined by its behavior rather than its class. This makes it easier to write generic code that works with a variety of objects.
  • Special methods: Python has a number of special methods that allow you to define custom behavior for built-in operations. For example, the
    __len__
    method allows you to define how the
    len()
    function works with objects of your class.
  • Readability: As with the general design principles of Python, classes should also prioritize readability and simplicity. This means using clear and concise names for methods and attributes, avoiding unnecessary complexity, and following PEP 8 guidelines for formatting.
Special methods

Special methods in Python are a set of predefined methods that allow you to define custom behavior for built-in operations on your objects. They are identified by their double underscore prefix and suffix, also known as "dunder" methods.

Here are some of the most commonly used special methods in Python:

  1. __init__(self, ...)
    : This is the constructor method that is called when an object is called. It initializes the object's attributes and sets its initial state.
  2. __str__(self)
    : This methods returns a string representation of the object. It's called by the
    str()
    function and by the
    print()
    statement.
  3. __repr__(self)
    : This method returns a string representation of the object that can be used to recreate the object. It is called by the
    repr()
    function and by the interactive Python shell. The difference between
    __str__
    and
    __repr__
    is that
    __repr__
    is used to create an unambiguous string representation of an object that can be used to recreate the object, while
    __str__
    is used to create a human-readable string representation of an object.
  4. __len__(self)
    : This method returns the length of the object. It is called by the
    len()
    function.
  5. __getitem__(self, key)
    : This method allows you to access an item in the object using bracket notation. It is called when you use the bracket operation(
    []
    ) on the object.
  6. __setitem__(self, key, value)
    : This method allows you to set the value of an item in the object using bracket notation. It is called when you use the bracket operation(
    []
    ) on the object with an assignment.
  7. __delitem__(self, key)
    : This method allows you to delete an item from the object using bracket notation. It is called when you use the
    del
    statement on the object with bracket notation.
  8. __add__(self, other)
    : This method allows you to define how the
    +
    operator works with your object. It is called when you use the
    +
    operator on the object.
  9. __eq__(self, other)
    : This method allows you to define how the
    ==
    operator works with your object. It is called when you use the
    ==
    operator on the object.
  10. __lt__(self, other)
    : This method allows you to define how the
    <
    operator works with your object. It is called when you use the
    <
    operator on the object.

There are many other special methods available in Python, but these are some of the most commonly used ones. By defining these methods in your classes, you can customize the behavior of your objects and make them more intuitive and convenient to use.

Example1:

class Person:
    def __init__(self, name, age, items):
        self.name = name
        self.age = age
        self.items = items
    
    def __str__(self):
        return f"{self.name} ({self.age})"
    
    def __repr__(self):
        return f"Person(name='{self.name}', age={self.age})"
    
    
person = Person("Alice", 25)
print(person)       # output: Alice (25)
print(repr(person)) # output: Person(name='Alice', age=25)

Example2:

class MyObject:
    def __init__(self, items):
        self.items = items
    
    def __len__(self):
        return len(self.items)
    
    def __getitem__(self, index):
        return self.items[index]
    
    def __setitem__(self, index, value):
        self.items[index] = value
    
    def __delitem__(self, index):
        del self.items[index]
    
    def __add__(self, other):
        new_items = self.items + other.items
        return MyObject(new_items)
    
    def __eq__(self, other):
        if isinstance(other, MyObject):
            return self.items == other.items
        return False
    
    def __lt__(self, other):
        if isinstance(other, MyObject):
            return len(self.items) < len(other.items)
        return NotImplemented
    
obj = MyObject([1, 2, 3])
print(len(obj))  # output: 3
print(obj[1])    # output: 2
obj[1] = 4
print(obj.items) # output: [1, 4, 3]
del obj[1]
print(obj.items) # output: [1, 3]
obj1 = MyObject([1, 2])
obj2 = MyObject([3, 4])
obj3 = obj1 + obj2
print(obj3.items) # output: [1, 2, 3, 4]

obj4 = MyObject([1, 2, 3])
obj5 = MyObject([4, 5])
obj6 = MyObject([1, 2, 3])

print(obj1 == obj2) # output: False
print(obj1 == obj3) # output: True

print(obj1 < obj2) # output: False
print(obj2 < obj1) # output: True
built-in functions
  • what's len() in len(obj) ?

In the context of
len(obj)
,
len()
is a built-in Python function that returns the length of an object.

The
len()
function can be applied to different types of objects in Python, such as strings, lists, tuples, dictionaries, and sets, and it returns the number of elements in the object.

For example,
len("Hello")
returns 5 because there are 5 characters in the string "Hello", and
len([1, 2, 3])
returns 3 because there are 3 elements in the list
[1, 2, 3]
.

In custom classes, the
len()
function can be used to retrieve the length of objects by implementing the
__len__()
special method, which should return an integer representing the length of the object.

  • more details about built-in Python functions
  1. print()
    : This function is used to print the specified message or object to the console. It can take multiple arguments, separated by commas, and can also use formatting to display variables and values.
  2. type()
    : This function returns the type of an object. It takes one argument and returns the type of that object.
  3. len()
    : This function returns the length of an object. It can be used with objects such as strings, lists, tuples, dictionaries, and sets.
  4. range()
    : This function generates a sequence of numbers, starting from a specified start value (by default 0), and incrementing by a specified step value (by default 1), up to but not including a specified stop value.
  5. input()
    : This function is used to get input from the user via the console. It takes one optional argument, which is the prompt to display to the user, and returns a string containing the user's input.
  6. int()
    ,
    float()
    ,
    str()
    : These functions are used to convert values from one type to another.
    int()
    converts a value to an integer,
    float()
    converts a value to a floating-point number, and
    str()
    converts a value to a string.
  7. max()
    ,
    min()
    : These functions are used to find the maximum or minimum value in a sequence. They can be used with objects such as lists, tuples, and sets.
  8. sum()
    : This function is used to find the sum of all the values in a sequence. It can be used with objects such as lists, tuples, and sets.
  9. sorted()
    : This function is used to sort a sequence in ascending order. It can be used with objects such as lists, tuples, and sets.
  10. zip()
    : This function is used to combine two or more sequences into a single sequence of tuples. It takes two or more arguments and returns a zip object containing tuples of elements from the input sequences.

These are just a few examples of the built-in Python functions. Python has many more built-in functions that can be used to perform a variety of tasks, such as manipulating strings, performing mathematical calculations, working with files and directories, and more.

前言

一、人物简介

  • 第一位闪亮登场,有请今后会一直教我们C语言的老师 —— 自在。

  • 第二位上场的是和我们一起学习的小白程序猿 —— 逍遥。

二、构成和表示方式

  • 逻辑运算符是用来比较和操作布尔值的运算符
  • C语言中的逻辑运算符主要有3个,如下表所示
运算符 名称 示例 描述
&& a && b 当a和b都为真时,返回真
|| a || b 只要a或者b,其中任何一个为真,返回真
! !a 如果a的条件为真,返回假
  • 逻辑运算符的两边可以是变量、数值 或 表达式

计算机图形中动态系统模拟最流行的方法是基于力的。累积内部和外部力量,根据牛顿的第二个运动定律计算加速度。然后使用时间积分方法来更新速度,最后是对象的位置。
一些模拟方法(大多数刚性体模拟器)使用基于冲量的方法并直接操纵速度。
PBD是一种省略了速度层的直接作用于位置的控制方法,方法的主要优点是它的可控性、性能、数值稳定,在游戏开发领域应用很广,近年的UE chaos底层也采用了PBD方法。

Verlet积分

verlet积分在游戏开发领域应用也很广泛,并且PBD论文中提到了PBD的思想和verlet有相近之处,这里也顺便介绍一下。

基本积分方法

积分方法的阶数一般是看泰勒展开中导数的最高项。n阶方法的误差是
\(O(h^{n+1})\)

值得一提的是,explicit euler、semi-implicit euler都是一阶方法。
积分方法讨论的通常是给定一个函数S(t),然后我们没有办法得到S(t)的解析式(假定这里是和时间有关的函数),此时我们通过一些方法来对S(t) 做近似,达到一个我们给定一个时刻的数据,可以推算其他时刻数据的效果,这里一切的理论基础都是泰勒展开,然后进行各种化简。
这里用图来表示,小红色的矩形代表积分过程,左边是explicit euler,右边是semi-implicit euler。

左图用的矩形的左边来表示一个小时间步里整体的情况,意思就是时间步刚开始的值。左图用的矩形的右边来表示一个小时间步里整体的情况,意思就是时间步结束时的值。

Verlet 算位置

verlet积分是一种牛顿运动方程的积分方法,他的积分方法有着良好的数值稳定性,对比简单的欧拉方法没有显著的额外计算成本。

\[ x _{t+\Delta t} = x_t + v(t)\Delta t + \frac{1}{2}a(t)\Delta t^2 + \frac{1}{6}b(t)\Delta t^3 + O(\Delta t^4)
\]

\[ x _{t-\Delta t} = x_t - v(t)\Delta t + \frac{1}{2}a(t)\Delta t^2 - \frac{1}{6}b(t)\Delta t^3 + O(\Delta t^4)
\]

两者求和,消去一阶和三阶项得到

\[ x _{t+\Delta t} = 2 x_t - x _{t-\Delta t} + a(t)\Delta t^2 + O(\Delta t^4)
\]

我们可以将
\(t+\Delta t\)
当成当前帧所在的时刻,那么t就是上一帧,
\(t-\Delta t\)
就是上上一帧,我们只需要两个位置信息和上一帧的力信息,就可以得到当前帧的位置信息,并且这个位置信息是三阶精度的,误差四阶精度,wiki百科中提到,这个方法这里的误差是局部误差,并且公式中的加速度是根据精确解计算的,全局误差(即累积误差)与时间步长的平方(Δt^2)成正比。虽然局部离散化误差为 4 阶,但由于微分方程的2阶,推导一番后全局误差为 2 阶,我们将它定义为2阶方法。

有的书上也说这个公式也可以从二阶中心差分推导出,所以是二阶方法。

Verlet 算速度

接下来是求速度信息,
两者求差,消去二阶项得到

\[
x(t+\Delta t)−x(t−\Delta t)=2v(t)\Delta t+ \frac{1}{3}b(t)\Delta t^3
\]

这样可以推算上一帧的速度信息

\[ v(t) = \frac {x(t+ \Delta t) - x(t - \Delta t)}{2\Delta t} + \frac{b(t)\Delta t^3}{3\Delta t}

\]

\[v(t) = \frac {x(t+ \Delta t) - x(t - \Delta t)}{2\Delta t} + O(\Delta t^2)
\]

这个算上一帧速度的方法是一阶精度二阶误差的。
在他的原始方法里推算当帧的速度用的中值定理+近似,导致速度一定是不准的,得出速度公式为

\[v(t+\Delta t) = \frac {x(t+ \Delta t) - x(t)}{\Delta t} + O(\Delta t)
\]

这个速度的准度可以说几乎没有,零阶精度,一阶误差,推导可以看verlet一开始
\(x(t+\Delta t)\)
的展开将xt移到左边再除以步长得到:

\[
\frac {x(t+\Delta t)−x(t)}{\Delta t} =v(t)+ \frac{1}{2}a(t)\Delta t + \frac{1}{6}b(t)\Delta t^2
\]

所以如果
\(\frac{1}{2}a(t)\Delta t + \frac{1}{6}b(t)\Delta t^2\)
作为误差,误差就是
\(O(\Delta t)\)
的。

wiki百科里则提供了一种更加高精度的速度推算方法叫 velocity verlet ,利用梯形法(头尾值加起来求平均,之前的小矩形可以想象成梯形来看)算的加速度来推算速度,让速度的精度到达二阶,至于为什么是二阶,因为梯形法的结果是二阶精度,这个精度貌似可以传递。

\[v(t+\Delta t) = v(t) + \frac {a(t)+a(t+ \Delta t)}{2} \Delta t
\]

a的
\(t+\Delta t\)
是一般来说是一个和位置有关的函数,先求了位置,接下来这里面都是已知量了。

PBD

基于力的方法解碰撞

首先一个直观印象关于如何处理动力学中的穿插问题,基于力的方法往往是计算穿插深度,然后给根据深度和穿插的方向给予力,这过程中有一个刚度系数,表示这个物体的坚硬程度,调的越大,两个物体越难穿插,你就会看到物体看起来很硬,实际航就是穿插产生的力的缩放系数,刚度越大实际上要求时间步长越小,否则很容易出现穿插过深弹出一个很大的力这种情况,其实就是数值解误差太大。

过冲问题

显式积分法,也称为开环法或单步法,广泛用于求解科学和工程各个领域的常微分方程 (ODE)。然而,这些方法可能会遇到过冲问题,这会导致不准确或不稳定的解决方案。
产生原因:
1、两刚体相交过深导致计算出来的力太大,求解的初始条件残差太大
2、时间步长太大
3、高频振荡:在求解涉及高频振荡的 ODE 时,显式方法可能难以准确捕获这些振荡,从而导致超调。在这种情况下,使用对刚性系统更稳定的隐式积分方法会有所帮助。
4、误差累积:使用显式方法时,误差会随着时间的推移而累积,从而导致显着的超调。

要解决显式积分中的过冲问题,请考虑使用更小的时间步长、自适应时间步长、高阶方法,或者在适当的时候切换到隐式或半隐式方法。此外,优化初始条件并使用灵敏度分析可以提高解决方案的准确性和稳定性。

基于位置的方法解碰撞

在PBD方法中,当检测到两个物体发生穿透时,直接根据约束修正物体位置到一个不会碰撞的位置,然后更新速度信息。他其实是一个反向的过程,虽然这中间力的计算不明确了,但是表现是正常的,这是我们期待的,这就是pbd论文提到的visually plausible 视觉正确,并且避免了之前提到的过冲问题。

算法流程

我们表达一个物体,使用N个顶点,M个约束,一个顶点i对应的质量是mi,位置则是xi,速度 vi
约束的索引一般使用j
我们对于其中一个约束有:

  • 定义一个约束影响的顶点数量是nj,可以理解为第 j个约束所影响的顶点数目为nj
  • 定义约束函数C 顶点数目为3nj,位置信息为xyz,所有的数据总量就是3nj,约束函数就是读取这个3nj的数据算出一个实数
  • 约束函数影响的顶点的索引,nj个索引
  • 定义这个约束的刚度值
  • 定义这个约束是等式约束和不等式约束
    等式约束比较强硬,要满足约束函数等于0,不等式约束则是约束函数大于等于0就行,kj定义了这个约束的刚度,刚度越大这个约束要越快被满足。

算法最开始是先初始化x和v,还有质量的倒数
每个循环开始,先通过力算速度,在通过算出的速度算位置,计算出一个预测的速度、位置。
然后在根据这个预测的位置情况进行碰撞约束的生成。
然后走一个固定的迭代次数,做约束投影,让p,其实就是位置,落在正确的 满足约束函数的地方,因为有多个约束,其实这里就是一个多个约束方程的方程组。
约束投影完了只有得到正确的位置,然后根据投影后的位置和上一次迭代的位置算一下速度(这里和verlet简单的算速度方法很像),然后把投影后的位置更新到位置数据里。
最后在(16)行根据摩擦(friction)和恢复(restitution)系数修改速度。

这个方案是无条件稳定的,因为他不会像一般的explicit方法那样对未来进行外插,而是将顶点移动到正确的位置。
他的稳定性不取决于时间步长,而是取决于约束函数的样子。
这个求解器并不像以往的显式或者隐式积分器,而是处于一种中间态,如果每个时间步只跑一个迭代他看起来像explict,而添加迭代次数,可以更像隐式积分方案。

求解器借用的思想

需要注意的是约束函数是通常都是非线性的,比如距离约束

\[C = |p_1-p_2| - d
\]

这里的非线性,其实指的是他这个方程组的未知数不是一次的,有绝对值的话,可以理解成平方再开方,一般的迭代法解线性方程组使用jacobi、gauss-seidel迭代方法,他们的未知数有多个,但是都是一次的。
但是在这里作者是借用了GS方法的思想,来解一个非线性方程组。
原gauss-seidel迭代法是,我们一整个方程组,然后从第一条方程开始,代入一个初值,然后方程左边算出第一个未知数X,然后这个X算出来后会直接替换掉下面方程组求解时用到X的地方,第二条方程算出Y,也是一样的,先满足一条方程,然后将它的影响带到下一条方程,经过固定次数的迭代后,系统的未知数们会收敛,就求解成功了。

值得注意的是,GS方案比较依赖方程的组的满足顺序,过度约束 且 没有保证求解顺序会导致结果振荡。

关于动量守恒

PBD方法里提到了动量守恒的话题,PBD本身方法的流程他是想说明按照他的流程走,只要约束函数定义时候是动量守恒的,那么结果是动量守恒的。
如果你这个系统最后输出的结果是没有违背动量守恒的基础,那么将看到一些不期待的运动,这里坐着定义为 Ghost force,看着就像有力量拖着物体 旋转物体一样。

后面作者提到,对于一个系统来说只有内部约束需要考虑动量守恒,因为外力作用会作用在全局而产生动量。内部约束不会改变刚体运动状态,即刚体的旋转、位移,作者称之为 与刚体模态 正交。
因此我们沿着约束函数梯度的方向去影响整个系统他最后满足约束了约束函数(满足这个内部力的性质),他的最后就是动量守恒的。
这里我需要说的详细的一点,因为我自己也对动量守恒这块比较困惑:
我的理解是PBD本身其实和动量守恒不沾边,主要是约束函数的定义是动量守恒的,你沿着约束函数的梯度去影响系统最后达到了约束函数,那么约束函数的最终状态是动量守恒的,我们拿这个状态去做表现,表现就是动量守恒的,但是如果我们因为很多约束,最后求解没成功,有很多约束没满足,那么最后的表现就是不动量守恒的。
如果我们定义了一个动量不守恒的C,最后求解成功了,那么表现出来也就是动量不守恒的,如果胡乱定义了一个约束还要求动量守恒 其实没有讨论的必要。

约束投影

这块公式相当多,
首先对于一个等式约束我们要求的就是这个状态代入约束函数后输出一个0

\[C(p+\Delta p) = 0
\]

而这个函数可以一阶泰勒展开得到

\[C(p+\Delta p) \approx C(p) +  \nabla_p C(p) \cdot \Delta p
\]

我们要把delta P限制在约束函数的梯度方向即

\[\Delta p = \lambda \nabla_p C(p)
\]

这个式子代入上一个式子得到λ
得到

\[\Delta p = - \frac{C(p)}{|\nabla_pC(p)|^2} \nabla_p C(p)
\]

这就是对于单个约束函数,整个系统接下来要做的改变。
而对于单个点p_i来说,前面的系数λ是一样的,后面的梯度方向则变成了这个delta P在单个点的分量,其实是梯度的某几个维度,也就是关于pi的偏导数
前面的系数用s表示,

\[\Delta p_i = - s \nabla_{p_i} C(p)
\]

\[ s = \frac{C(p)}{\sum_j {|\nabla_{p_j}C(p) |^2}}
\]

其实这里下面的分母就是梯度的模长,梯度的模长等于各个偏导数分量的平方和。
特别注意下维度:
这里的p是指所有的点的位置,维度是点的数量 n * 3 ,其中delta P会保证和约束函数的梯度一样的方向,约束函数的梯度也是一个 n * 3的向量 。

delta P1则表示delta P向量的第一点分量,是一个3分量的向量,其实就是第一个点的位移,他应该保证和约束函数在这个第一个点的偏导数保证方向相同。

简单约束举例

这里就不举例了,可以查看参考文章里面的距离约束
https://zhuanlan.zhihu.com/p/48737753

刚度体现

公式里还有一个k参数,这个参数的作用意会一下就是,我们有一条约束方程,要以怎么样的速度去满足这条方程,如果k等于1则说明直接满足,不留余地,等于0-1的值,则说明可能是跑几轮才能比较接近精确解。
刚度越大,越快满足约束方程,也就和刚度体现的越硬对应了。
一个问题是,他自己指出材料的刚度依赖时间步长,定步长就不太会有这些问题,verlet积分的推断也是依赖定步长。
多次迭代下误差为

\[ \Delta p (1-k)^{n_s}
\]

另外,为了消除多次迭代中残差随着次数指数级增长的问题,他这里用的一个非常怪的式子反推了,然后误差变为线性依赖于k而不依赖于ns,ns是迭代次数。

\[k' = 1-(1-k)^{\frac{1}{n_s}}
\]

最终误差来到

\[\Delta p (1-k')^{n_s} = \Delta p(1-k)
\]