# 开发日志

官方例程链接: https://gitee.com/kit-miao/dm-mc02

## 0 要做的事情

- ==开发底层驱动==
  - ==标题开头要改版本号==
  - ==修正外设的逻辑==
  - ~~DMA前后加MPU~~
  - ==开发USB==

- 开发板载设备的BSP

  - ==串口绘图加入USB==
  - ==指示灯ws2812==
  - ==蜂鸣器==
  - ==按键检测==
  - ==电源电压检测与可控电源输出, 两个24V一个5V==
  - IMU
    - ==时间库==
    - ==通信接口加时间戳==
    - ==数据读取==
    - ==通信回调函数规范化, 增加状态变量==
    - ==大结构体传引用==
    - ==阅读手册与寄存器, 分析配置流程==
    - ==矩阵运算库, 向量运算库, 四元数运算库==
    - ==卡方检验==
    - ==温控==
    - ==量测加速度计和陀螺仪的均值方差==
    - ==vofa+接口开发==
    - ==校正陀螺仪==
    - ==校正加速度计==
  - ==OSPI与Flash开发==
  - ==SPI完善, 加入仅发不收选项==
  - 电机
    - ==就近转位==
    - ~~反向~~
    - ==扭矩控制==

## 1 杂项

- 电脑编译速度奇慢的话可以打开任务管理器, 关闭MSPCManagerService, 可以大幅提高编译速度

- 复位前记得关闭中断

  ```C
  __set_FAULTMASK(1); // 关闭所有中断
  NVIC_SystemReset(); // 触发系统复位
  ```

- 代码习惯

  - 类的撰写
    - 尽量传引用
    - 不改变成员的函数加const, 比如传参, 函数后const等, 指针不加const
    - 带模板的, 初始化函数声明定义, 运算符重载都在类内, get, set函数在h文件内, 其它函数在cpp内

## 2 内存保护

- 稍微了解就行, ==由于大多数开发者对该内容掌握较为生疏, 且改动后易造成难以调试的bug, 因此在本开源项目中不建议加内存保护单元的内容==

- 内存分配情况

  <img src="开发日志.assets/image-20250812022336288.png" alt="image-20250812022336288" style="zoom:50%;" />

  <img src="开发日志.assets/f9ce58bc476ced44c53071cf313ef816.png" alt="img"  />

- H7特有的

  - 可加快读写速度
  - 缓存, 缓冲, 共享
    - CPU到Cache, 可选缓存 ( 没开缓存则相当于没开Cache提升性能 )
    - CPU到内存, 有Buffer缓冲
    - Cache到内存, 可选Buffer缓冲
    - 总线主机到从机之间, 可选共享 ( 开了共享则相当于没开Cache, 没开共享可能导致数据更新不及时问题 )
  - Cache读写操作
    - 读命中, 直接读Cache就行
    - 读脱靶, 两种策略
      - 读通, 直接读内存不用Cache
      - 读分配, 加载数据到Cache后再读Cache, 性能好一点
    - 写命中, 两种策略
      - 写通, 直接写Cache并写内存, 体现不出优势
      - 写回, 只写Cache, 等数据替换出Cache时同步回内存, 性能好一点
    - 写脱靶, 两种策略
      - 写分配, 加载数据到Cache后再写Cache并写内存, 性能好一点
      - 非写分配, 直接写内存
  - **涉及DMA会引入内存不一致问题**
    - 设置共享属性, 相当于没开Cache
    - 软件维护Cache, 发生在SRAM数据在Cache变化后未更新
      - 操作有点耗时, 不要在中断里
      - DMA搬数据前
        - SCB_CleanDCache_by_Addr将D-Cache里的数据写回到实际存储区SRAM里
      - DMA收到数据后
        - SCB_InvalidateDCache_by_Addr将实际存储区SRAM的数据更新到D-Cache里
  
- CPU的I和DCache开启

- MPU Control Mode选择MPU_HFNMI_PRIVDEF, 高可靠性系统如汽车电子, 需在异常中严格保护内存

  <img src="开发日志.assets/image-20250811154820791.png" alt="image-20250811154820791" style="zoom: 67%;" />

- 开一块MPU, 对应AXI内存
  
  <img src="开发日志.assets/image-20250813084427639.png" alt="image-20250813084427639" style="zoom:50%;" />

## 3 UART

- 以前发现DR16遥控器有概率粘包, 而图传链路也有这个现象. 查看辽科开源电控代码, 发现他们配置是9E2, 经过交流发现这样可以解决丢包问题
- 分析可能的原因: 9E1的配置下, 在物理链路上的停止位还没有结束时, 单片机即认为接收完毕. 而遇到后半段停止位, 单片机以为是新的一轮发送, 因此会触发接收

## 4 LED灯

<img src="开发日志.assets/image-20250815023745432.png" alt="image-20250815023745432" style="zoom:50%;" />

- 达妙官方例程波特率不太合理, 导致1000ns压着线的, 容易出错
- 修改为6.25Mb/s即可, 即160ns一个bit微元, 1B对应1280ns, 2+6与4+4对应低高电平
- 正常情况下是0xc0和0xf0, 但可能涉及电平稳定问题, 需要将数据右移一位, 与此同时发送完成之后不要急着停, 再发送1字节的0保证RES稳定

- 注意内存访问矩阵, 这个可能是DMA无法正常发送的原因

  > 由于SPI6需要靠BDMA传输, 而BDMA只能访问SRAM4, 即外置SRAM, 因此如果内存未特殊绑定的情况下, 在默认位置的数据只能使用阻塞传输

  <img src="开发日志.assets/3512806-20250327155845289-30493235.png" alt="img" style="zoom:50%;" />

## 5 IMU开发

- 特性
  - 有Power On Reset ( POR ) 机制, 上电复位全部数据都清空为初始状态, 因此掉电需要重新配置
  - 加速度计需要先片选一次使能SPI, 而后读一次ACC_CHIP_ID寄存器
  - 此时, 陀螺仪是normal可以直接使用, 而加速度计是suspend, 需要额外配置
    - 上电
    - 1ms
    - 给ACC_PWR_CTRL写入4
    - 50ms
  - 配置完这些才能初步使用
- 我的配置流程
  - 看手册
    - 常规情况下, 可以定时器中断自动读取数据
      - 如果和对应的加速度计或陀螺仪同频, 则可能导致数据未准备好即读取, 主动引入错误时间戳
    - IMU支持两种模式
      - FIFO模式
        - 数据会一股脑存在某个缓冲区内, 某个引脚会给个中断, 而后SPI去读
        - 少次大量, 适合复盘示教
      - 中断模式
        - 数据准备好了, 某个引脚会给个中断, 而后SPI去读
        - 多次小量, 实时性高
        - ==因此采用中断模式==
  - 加速度计, 1600Hz, 625us
    - 开启加速度计电源
    - 将加速度计从默认挂起状态0x03改为工作状态0x00
    - 加速度计无滤波器, 频率1600Hz
    - 加速度计量程±24g
    - 中断1号引脚配置推挽输出模式
    - 配置为如果数据准备好就中断
  - 陀螺仪, 2000Hz, 500us
    - 设置陀螺仪量程2000°/s
    - 设置陀螺仪反馈频率为2000Hz, 带宽为532Hz, 0x80是只读位, 永久为该值, 可忽略
    - 配置为中断输出模式
    - 中断1号引脚配置推挽输出模式, 实际上写0才是配置部分, 默认是全1
    - 配置为如果数据准备好就中断
- 每轮数据请求的状态变化
  - BMI088触发EXTI回调函数指示数据准备完成, 并记录时间戳
    - DR=1
  - 定时器中断回调函数内轮询状态是否满足 " 有中断数据但还没收发 " , 若满足则SPI使能DMA收发. 定时器周期为容忍的数据延迟周期, 由于一轮收发完成约8B, 64b, 对应10Mb/s是6.4us, 因此用10us轮询即可
    - Trans=1, self, DR=0
  - SPI回调函数接收处理, 记录时间戳保证EKF时间确定
    - self, Trans=0, UPDATE=1
  - 计算EKF, 由于工作频率分别是1600和2000Hz, 因此取最小公倍数8000Hz进行计算EKF, 进行朴素地时间戳对齐
    - self, UPDATE=0
  - 闲置, 等待下一轮
- 关于校正
  - 一切数据在50℃下进行采集
  - 采集时需静止
  - 陀螺仪需测量十分钟数据
  - 加速度计需测量不同姿态数据指天
    - 单轴向
    - 双轴向
    - 三轴向

## 6 Flash开发

- 高性能点到点

- Flash最高133MHz

- 32768个Page

  - 每个Page 256B, 每次最多可写入一个Page
  - 每16个Page一Sector, 每Sector 4KB, 整个芯片等价有2048个Sector
  - 每16个Sector一Block, 每Block 64KB, 整个芯片等价有128个Block
  - Page->Sector->Block

- 擦除逻辑

  - 整个芯片可一次性擦除, 除以Page和Block为单位擦除外, 还支持以Half Page为单位擦除

- 读取逻辑

  - 可以一个地址后一直读下去

- 使能QSPI需要额外配置状态寄存器

- 写保护机制

  - 固定写保护

    - 芯片上电默认写保护, 写入数据前, 须先发==写使能==指令
    - 完成一次写入, 自动恢复到禁止写状态

  - 寄存器决定的写保护

    - 设置SRP, SRL和CMP, TB, BP[3:0], 可实现软件控制的写保护功能. 可将部分或全部存储阵列配置为只读模式

      ![image-20251013070736565](开发日志.assets/image-20251013070736565.png)

      

      ![image-20251013185226729](开发日志.assets/image-20251013185226729.png)

  - 独立块锁决定的写保护

    - 每个64KB块, 顶部和底部块除外的每个Block ( 共126个Block ) , 以及顶部/底部块内的每个Sector ( 共32个Sector ) 都配备了一个独立块锁位
      - 锁定位为0, 可以进行擦除或编程操作
      - 锁定位设为1, 发往该区域的擦除或编程命令将被忽略
      - 器件上电时, 所有锁位均为1, 因此整个存储阵列默认处于擦除/编程保护状态. 必须发送解锁指令才能解除特定扇区或块的锁定

- 状态寄存器

  - 状态寄存器1

    - 忙信号, BUSY, [0]
      
      - 只读
    - 写使能, WEL, [1]
      - 只读
      - ==上电后或写入数据后为1, 手动置0后才能写入数据==
    - Block保护位, BP, [4:2]
      - 非易失读写
      - 出厂默认0, 不保护
      - 我们用的时候也默认0, 不进行保护
    - 顶部底部保护位, TB, [5]
      - 非易失读写
      - 控制BP是从阵列的顶部TB=0, 还是底部TB=1开始进行保护
      - 出厂默认设置为顶部TB=0
    - Sector/Block保护位, SEC, [6]
      - 非易失读写
      - 控制BP是以Block为单位SEC=0, 还是以Sector为单位SEC=1进行保护
    - 状态寄存器保护, SRP, [7]
      - 非易失读写
      - 结合SRL寄存器和WP引脚综合配置
  - SRP控制WP引脚是否起作用, ==达妙板上, WP引脚悬空==
  
- 状态寄存器保护, SRL, [8]
  
      - 非易失读写
      - SRL控制是否单次上电锁定
  - 一般我们用第一种默认的即可, 即单次上电可读写, WP引脚不起作用
  
- 四线SPI使能位, QE, [9]
  
      - 非易失读写  
      - 用于启用Quad SPI操作
      - 当QE位被设置为0状态时, /HOLD功能被启用, 器件工作于标准/Dual SPI模式
  - 当QE位被设置为1时, Quad IO2和IO3引脚被启用, /HOLD功能被禁用, 器件工作于标准/Dual/Quad SPI模式
  
- 安全寄存器锁, LB, [13:11]
  
      - 非易失==一次性==可写
      - 提供对安全寄存器的写保护控制和状态指示, 默认状态为0, 表示安全寄存器处于解锁状态
      - 可通过写状态寄存器指令分别将这三位设置为1
  - 一次性可编程位, 一旦被设置为1, 对应的安全寄存器将永久只读
  
- 补码保护, CMP, [14]
  
      - 非易失读写
      - 与SEC、TB、BP2、BP1和BP0位配合使用，以提供更灵活的阵列保护功能, 默认设置为CMP=0
      - 当CMP设置为1，之前由SEC、TB、BP2、BP1和BP0设定的阵列保护将被反转
    - 例如, 当CMP=0时, 顶部64KB块受到保护而其余阵列不受保护; 当CMP=1时, 顶部64KB块将不再受保护, 而其余阵列则变为只读
  
- 擦除/编程暂停状态, SUS, [15] 
  
      - 只读
      - 执行擦除/编程暂停后被置为1
  - 执行擦除/编程恢复指令或经历掉电再上电后清零
  
- 写保护选择, WPS, [18]
  
      - 非易失读写
      - WPS=0, 器件将使用CMP, SEC, TB, BP[2:0]组合来保护存储阵列的特定区域
  - WPS=1, 器件将使用独立块锁来保护任意单个Sector或Block, 所有独立块锁位在器件上电或复位后的默认值均为1
  
- 输出驱动强度, DRV, [22:21]
  
  - 非易失性读写
    
    ![image-20251013152314819](开发日志.assets/image-20251013152314819.png)
    
      - 确定读操作的输出电平强度

- 关于OSPI

  ```C++
  HAL_StatusTypeDef     HAL_OSPI_Command_IT(OSPI_HandleTypeDef *hospi, OSPI_RegularCmdTypeDef *cmd);
  HAL_StatusTypeDef     HAL_OSPI_Transmit_DMA(OSPI_HandleTypeDef *hospi, uint8_t *pData);
  HAL_StatusTypeDef     HAL_OSPI_Receive_DMA(OSPI_HandleTypeDef *hospi, uint8_t *pData);
  HAL_StatusTypeDef     HAL_OSPI_AutoPolling_IT(OSPI_HandleTypeDef *hospi, OSPI_AutoPollingTypeDef *cfg);
  
  void                  HAL_OSPI_StatusMatchCallback(OSPI_HandleTypeDef *hospi);
  void                  HAL_OSPI_RxCpltCallback(OSPI_HandleTypeDef *hospi);
  ```

  

  