用C进行嵌入式开发已经落伍了,现在是面向对象与函数式编程的时代。外设天然就是一个个类,传统的面向过程与全局变量乱飞的思维显然已经不适用于编写出安全高效可复用性强的代码。
我目睹了编写着结构体以及调用初始化函数的不胜其烦的没有统一函数名的各家标准固件库;看似统一了硬件实则在库中大量使用宏去重新对不同的硬件平台作细分,缺乏对底层的精细化控制的毫无美感的Java风格C++的Arduino;与尚未摆脱真正的硬件抽象仅仅局限与STM32的HAL库,感叹嵌入式已经到了今天这样不堪的地步吗。起始不然,实时,低成本,分布式的嵌入式系统可以遍布各处,但实际上嵌入式开发夕阳西下被人唾弃,根本原因是大部分嵌入式开发中在作斗争的是编写对应平台SDK驱动的代码,而不是在其上部署高性能的算法,或许那些作顶层开发从不正眼瞧底层与写着屎山代码的“行业老者”从来没有发现嵌入式可以这样美。
利用C++的高级语法,我们可以让嵌入式开发从这些令人作呕的外设配置变成了调度抽象语义的游戏,才能够使我们编写出更快更清爽更加与现代设计模式接轨的代码,不是吗?
很多人说C++不适合用在嵌入式上,更别说STL了。实际上这样的观点大错特错,实际上我们完全能在嵌入式开发上享用最新的特性且随处使用STL。只要我们提高优化选项,手动重写nanolib(离开printf),禁用dynamic_cast,禁用浮点改用定点,禁用异常等本就不健康的开发方式,C++的代码产生的二进制文件完全可以部署在嵌入式上。在本框架上,完全可以在32Kb不到的芯片上编写大量业务代码而不超过芯片的极限。例如,通过本框架可以在短时间内部署大量低成本节点进行组网并实现传感,控制,计算等组件的模块化。
下面是一些测试效果:
作用 | 描述 | 开销 | 性能 |
---|---|---|---|
步进电机FOC驱动器 | 基于CH32V203(对标F103)芯片实现了完整的Can节点及FOC控制 | 45KB / 64KB | 电流环/速度环/位置环均能以40khz+运行 |
机器人视觉主控板 | 基于CH32V307(对标F407)芯片实现了完整的Can主机以及视觉/AI/行为树的运行 | 128KB / 192KB | 能以60fps运行视觉算法,自动完成对目标的搜索 |
九轴传感节点 | 基于CH32V203(对标F103)芯片实现了完整的Can节点及串口命令行 | 28KB / 32KB | 微秒级解算 |
本库相较主流开发框架的优势:
vs arduino:
- 提供了对外设每一分细节的精细控制,不需要再用宏定义区分各个平台
- 提供了统一的设备管理层
- 提供了统一的抽象层 所有同类外设均有对应的抽象类
- 提供了对常用总线的原生支持,而不是全局单例
- C++化程度更高,减少Java风格的糟糕体验
- 大量使用模板与泛型,以实现代码的复用与泛化
- 大量使用stl(参考odrive)
vs HAL:
- 做到了真正的硬件抽象,对不同的芯片都提供的近乎相同的api
- 应用层与库函数完全隔离,在应用层完全不需要和厂方SDK进行交互
- 没有丑陋的HAL前缀表示函数的命名空间
- 用多态取或lambda取代函数指针的使用,减少了心智负担
- 所有外设通过用户自定义宏裁剪而不是工程文件管理 提供了更高的自由度
- 将自动化生成代码与用户业务代码隔离
无需复杂的结构体配置点亮一盏led
int main(){
//...prework()
//通过引用获取GPIO
//端口使用工厂模式返回GPIO,确保GPIO不会被随意构造
auto & led = portC[13];
//将led设置为输出模式,并以高电平初始化
led.outpp(HIGH);
while(true){
//通过重载GPIO的布尔运算符以写入与读取,使得GPIO能够直接被布尔量赋值
led = !led;
delay(200);
}
}
又或是开启互补PWM快速验证SPWM输出
int main(){
//...prework()
//将定时器①以36Khz初始化 使用字面量常量表达式来计算
timer1.init(36_KHz);
//使用工厂模式来获取定时器①的输出比较通道①
auto & pwm = timer1.oc(1);
pwm.init();
{
//获取互补PWM通道并初始化
auto & pwmn = timer1.ocn(1);
pwmn.init();
//或者也可以直接
timer1.ocn(1).init();
}
//将刹车寄存器初始化 默认为200ns
timer1.initBdtr();
//进入主循环
while(true){
// t(时间) 是一个全局变量 将在systick中被自动更改
// 重载了PWM被数值赋值的函数 使得对它的赋值能直接改变占空比
pwm = 0.5 + 0.5 * sin(t);
}
//或者也可以直接这样做
//以0:0的中断优先级将定时器①的更新中断使能
//使用枚举类以确保安全
timer1.enableIt(TimerIT::Update, {0,0});
//使用lambda函数建立对应中断请求的回调函数
timer1.bindCb(TimerIT::Update, [&](){pwm = 0.5 + 0.5 * sin(t);});
}
再或者通过CAN指令精细控制云台的角度
int main(){
//...prework()
//将CAN1以1Mbps的波特率初始化
can1.init(1_MHz);
//将串口一以921600的波特率初始化 默任TX和RX都使用DMA进行数据收发
uart1.init(921600);
//OutputStream是串口的基类 可以实现数据的打印
OutputStream & logger = uart1;
//建立舵机与步进电机变量
SG90 servo {timer1.ch(1)};
DRV8825 stepper {timer1.ch(2);}
//通过引用的方式建立is-a的关系 其中多继承确保只要是两个继承自角度可控类的对象均可以传入云台中
Gimbal gimbal {servo, stepper};
gimbal.init();
real_t angle;
while(true){
if(can1.available()){
//从CAN的接收FIFO中读取报文
const auto & msg = can1.read();
//使用模板以确保报文可以转换为任意类型的数据
msg.load(angle);
//直接就能将数据打印到串口
logger.println("angle is:", angle);
}
//内部有多种向量数据类型(多维向量与四元数等),为机器人学奠定基础
gimbal.moveTo(Vector2(1, 0).rotated(angle));
}
}
当我们用面向对象的方式构建起代码时 一切是如此自然且清晰。 我们脱离了厂家的SDK,在业务代码中就不应该和具体平台绑定。
目前受到完整支持的芯片只有CH32V2/CH32V3系列,正在考虑添加将文件进一步分层,加入非SXX32风格的芯片乃至支持Linux
使用C++20编写 请使用gcc12及以上的版本进行编译 使用Vscode下的eide搭建开发环境
厂方sdk库已改动 请使用用本仓库提供的sdk 位于sdk目录下下
大量使用iqmath在无浮点单元的情况下进行高速数学运算 请选择16位IQ数(调整宏定义GLOBAL_Q)否则可能会出现意想不到的问题
算是用来练习git的项目, 第一次使用git不太熟悉 孩子自个写着玩的 本框架全由作者一人编写,参考了其他代码。 由于目前项目架构尚不稳定 不要轻易fork!!!!!
由于本项目使用GCC进行开发(对各平台实现最大化开源),目前不打算增加对其他编译器的支持(ARMCC/ARMCLANG不支持) 且本项目使用到了完整的C++20的特性进行开发,故GCC的版本不低于12
CH32系列开发:
同时由于WCH为了支持快速中断,为GCC添加了魔改的中断关键字,故应使用WCH定制的GCC12(需要安装MounRiver Studio以获取我们所需要的厂家魔改版riscv-none-elf-gcc(买椟还珠属于是了))
为了提供插件EIDE的宿主以及获得遥遥领先的编码体验 需要下载vscode并安装
为了提供方便快捷开箱即用的环境体验 本项目选择vscode的插件eide进行开发 具体请移步 eide
为了支持WCH-Link基于OPENOCD进行了定制化开发故需要安装WCH提供的OpenOCD(同样在MounRiver Studio的安装目录中可以找到)
请使用Os/O3/Ofast进行编译,以确保代码的尺寸和运行速度被尽可能优化, 否则FLASH占用不足以支持使用
-
random 伪随机数发生器
-
hash 哈希函数
-
interpolation 插值
- 线性插值
- 三次插值
- 四次插值
- 多项式插值
- 弧形插值
-
encrypt 加密解密
- SHA256编码解码
- AES编码解码
- BASE64编码解码
- CRC编码解码
-
astar A*寻路算法
-
编译期超越函数计算
-
控制器
- PID 控制器
- 模糊PID控制器
- LQR 控制器
- MPC 控制器
-
fft(未测试)
-
滤波器
- 卡尔曼滤波器
- 拓展卡尔曼滤波器
- 低通滤波器
- 高通滤波器
- 施密特触发器
- 抖动滤波器
- 毛刺滤波器
-
cordic 三角运算单元
-
观测器
- 一次观测器
-
线性时不变系统
-
查找表
-
振荡器
-
pll 锁相环
-
sogi
-
aabb 三维包围盒(godot)
-
Arc2D 二维弧形元素
-
basis 三维旋转矩阵(godot)
-
Bezier2D 二维贝塞尔曲线
-
buffer 缓冲结构
- fifo 环形缓冲
- lifo 栈
-
Color RGBA颜色表述(godot)
-
Complex 复数
-
Curve2D 二维曲线类
-
Image 图像类
- Font 字体类
- 英文字体
- 中文字体
- PackedImage 压缩二值化图片
- Painter 绘图算法驱动
- Font 字体类
-
Line2D 二维直线类
-
matrix 矩阵
- matrix_dynamic 动态矩阵
- matrix_static 静态矩阵
- 加减乘法
- 求转置/逆
- 求行列式
- 方法库
-
plane 三维平面类(godot)
-
Point2D 二维点类
-
polar 极坐标类
-
quat 四元数类(godot)
-
transform3d 三维位姿矩阵(godot)
-
dh 连接件DH参数
-
range 区间类
-
Ray2D 二维射线类
-
rect2 矩形区域类
-
Segment2D 二维线段类
-
Tranform2D 二维变换矩阵类
-
Tranform3D 三维变换矩阵类
-
vector2 二维向量类(godot)
-
vector3 三维向量类(godot)
-
rgb 各类颜色结构体
-
颜色空间转换
- 布尔运算
-
形态学
-
自适应阈值化
-
CANNY
-
卷积核
-
洪水填充算法
-
边线提取
-
开闭运算
-
仿射变换
-
模板匹配
-
Mnist深度学习识别
-
apriltag识别
-
霍夫变换
-
-
执行器
-
线圈
- 单端线圈
- 双端线圈
- 三端线圈
-
驱动
- AT8222
- DRV8301
- DRV8313
- EG2103
- EG2104
- EG2133
- MP1907
- MP6540
- TB67H450
-
变换器
- DRV2605L
- SC8721
-
SVPWM
- SVPWM
- SVPWM2
- SVPWM3
-
云台
-
舵机
-
-
ADC
- ADS112C04
- ADS1115
- HX711
- INA226
- INA3221
- SGM58031
-
DAC
- TM8211
-
音频
- JQ8900
-
DDS
- AD5933
- AD7607
- AD9833
- AD9854
- AD9910
- AD9959
- SI5351
-
摄像头
- MT9V034
- OV2640
-
屏幕
- SSD1306(OLED)
- STN7567
- ST7789(tft)
-
编码器
-
磁编码器
- AS5047
- AS5600
- MA730
- MT6701
- MT6816
- MT6835
-
AB编码器
-
PWM编码器
-
模拟编码器
-
里程计
-
-
HID设备
- PS2手柄
-
IMU
- 六轴
- ADXL345
- BMI088
- BMI160
- BMI270
- ICM42605
- ICM42688
- MPU6050
- 地磁
- AK8975
- BMM150
- HMC5883L
- IST8310
- MMC5603
- QMC5883L
- 六轴
-
环境光传感器
- BH1750
- TCS34725
-
气压计
- BMP280
- BMP180
-
存储器
- EEPROM(AT24)
- FLASH(W25)
- SD卡
-
调制器
- DSHOT
- NEC
-
空间感知
- LDS14
- PAJ7620
- VL53L0X
- PMW3901
- SR04
- VL6180X
-
识别器
- U13T
-
一般IO
- LED
- 模拟LED
- 按键
- 按键矩阵
- LED
-
虚拟IO
- AS9523
- HC138
- HC165
- HC595
- PCA9695
- PCF8574
- TTP229
-
无线
- CH9141
- HC12
- LT8920
- LT8960
- LT8960
- Si24R1
- XL2400
-
arch 架构
- arm
- cm3
- cm4
- riscv
- qingkeV3
- qingkeV4
- arm
-
constants 编译期常量
- concepts c++20概念约束拓展
- enums 内置枚举类型
- uints 单位转换
-
clock 时钟
- 毫秒 微秒 纳秒级时间戳
- Systick回调函数
-
tasker 非抢占式任务驱动器
-
错误处理
- Result(rust)
-
多态
- proxy3
-
Stream(输入输出流)
-
printf
cout
(内建输出流操作) -
println
print
prints
<<
(基本输出流操作)- 直接将类型格式化输出
- 将各输出类型重载输出
- 重载了容器的输出
- 添加对std::hex, std::setpos, std::setprecision等函数的支持
-
-
String 字符串类(arduino)
- string (字符串主体)
- string_view (字符串视图)
- string_ref (字符串引用)
- string_utils (字符串工具)
-
math 数学类型库
-
IQ(IQMATH)
- 支持四则运算 大小比较 类型转换
- 支持超越函数
- 支持std::超越函数
- 支持concept
- 使用模板重构
- 可选的浮点数杜绝机制
-
float 各类浮点数
- fp32
- bf16
- fp16
- fp8
- fp8e4m3
- fp8e5m2
-
int 整数
- int(rust)
-
- 宏定义头文件
- example/testbench
- main文件
-
GPIO(IO相关代码)
- bitband(位带操作)
- Gpio(单个IO)
- Port(IO端口)
- VirtualPort(虚拟IO端口)(用于将多个片上与片外端口按顺序绑定至一个IO端口上)
-
UART(已验证1500000波特率的长时间压力测试)
- 对基本输入输出流的支持
- 环形缓冲区支持
- DMA/中断支持
- LinBus/智能卡
-
SPI(已验证144Mhz的长时间压力测试)
- 一般数据收发
- DMA数据收发
- 虚拟片选集线器
- 软件SPI
-
I2C(以验证5.4Mhz的软件I2C)
- 软件I2C
- 硬件I2C
- 一般数据收发
- DMA数据收发
- SMbus
-
I2S(以验证软件I2S只发)
- 软件I2S只发
- TM8211
- 硬件I2S
- I2S读取
-
CAN(控制局域网总线)
- 信箱及FIFO驱动
- 输入输出环形缓冲区
- 标准ID与拓展ID下Msg发送接收
- 标准ID下的过滤器
- 基于模板的自定义过滤器
-
Adc
- 任意通道单路信号采集
- 任意通道多路信号采集
- DMA绑定API
- 片上温度/参考电压采集
- 虚拟ADCChannel类
-
DVP
- DVP总线驱动
-
CRC
-
OPA
-
NVIC
-
EXTI
-
FLASH
- 快速FLASH读写
-
TIM(定时器相关代码)
-
中断与回调函数绑定
-
编码器模式
-
PWMModule PWM输出集线器
-
PWMChannel PWM输出概念
- GPIOPwm 使用GPIO配合定时触发模拟pwm
- TimerOC(定时器输出)
- TimerOC
-
CaptureChannel 输入捕获概念
- ExtiCapture 基于EXTI的输入捕获
- TimerIC(定时器输入捕获)
-
-
TCP/UDP
-
USB
- USBFS USBFS虚拟串口驱动
-
BLE
-
FOC
-
步进电机FOC算法
-
电流环
-
力矩环
-
速度环
- 低速速度环(0rpm~240rpm)
- 中速速度环(300rpm~2400rpm)
- 高速速度环(2400rpm~8000rpm)
-
位置环
- 高精度位置环
- 常规位置环
-
自校准算法
-
自检查算法
-
基于串口的RPC
-
基于CAN的RPC
-
数据存档
-
高级插值规划
- 直线规划
- 正弦规划
- S形规划
-
-
无刷电机FOC算法
-
-
正逆解
- Scara正逆解
- 轮腿正逆解
- 交叉臂正逆解
- 六轴正逆解
-
DJI RoboMaster相关驱动
- M2006
- M3508
- M6020
- DR16
-
变换器
-
halfbridge
通用半桥驱动 -
buck.hpp
BUCK驱动 -
pmdc.hpp
直流有刷电机驱动算法 -
spread_cycle.hpp
对SpeadCycle斩波驱动的低劣模仿
-
-
机械
- 平动滑台描述
- 转动关节描述
-
rpc rpc框架
- 串口 RPC
- CAN RPC
-
AI
- 神经元
- 网络
- 复合网络
- 训练器
- 损失函数
-
光栅器
- 任务队列
- 光栅化器
-
策略
-
动作池
- 运动队列
- 单个运动
- 组合运动
-
行为树
-
复合节点
- 选择节点
- 顺序节点
- 全通节点
-
装饰器
-
-
FSM
-