项目介绍
图一 项目前视图动图(时钟模式)
这是一个桌面摆件,利用磁流体作为表针,通过控制磁铁磁场精准操控其运动,从而实现指针功能的项目。
这个磁流体桌面摆件有两种模式:一是时钟模式,分别在三个表盘上显示小时、分钟和秒(见图一);二是环境模式,分别在三个表盘上显示温度、湿度和噪声强度。
此外,我还在一块仅售两元的、资源极其受限的STM32G030F6P6单片机上,成功实现了基于C语言的四阶龙格-库塔法的双摆和Lorenz attractor两个混沌系统的实时数值仿真,并在OLED屏幕上展示仿真结果。
磁流体介绍
磁流体又称磁性液体、铁磁流体或磁液,它既具有液体的流动性又具有固体磁性材料的磁性。
- 磁流体由两部分组成,一部分是直径为纳米量级(10纳米以下)的磁性固体颗粒,通常为纳米铁或者纳米四氧化三铁;另一部分是基载液,也叫媒体,常用的媒体有水、有机溶剂、油等。
- 磁流体在静态时无磁性吸引力,当外加磁场作用时,才表现出磁性,看起来就像电影中的“毒液”(见图二)。
- 用纳米金属及合金粉末生产的磁流体性能优异,可广泛应用于各种苛刻条件的磁性流体密封、减震、医疗器械、声音调节、光显示、磁流体选矿等领域。
图而 磁流体外观
同类产品及项目出发点
磁流体在磁场作用下表现出类似于液体的流动特性,这一现象与某些科幻作品中的“毒液”形象有相似之处。当前市场上已存在多种磁流体时钟设计,这些设计旨在通过磁流体的动态展示来象征“时间如水”的概念。如图所示,这些产品中既有利用电磁铁吸引磁流体形成特定图案的方案,也有采用永磁体阵列来吸引磁流体的方案。然而,无论采用哪种方案,这些磁流体时钟大多作为独特的艺术品进行展示,或者因其高昂的价格(例如,图三b中所示的淘宝售价为3600元)而限制了其普及性,甚至有售价高达5万元人民币的全球限量款。对于DIY爱好者而言,他们更多地利用电磁铁来制作磁流体音响,这种玩法虽然有趣,但难以实现复杂信息的显示。
图三 目前市面上已经有的一些围绕磁流体展开的电子设计
本项目提出了一种新颖的磁流体时钟设计方案,该方案与前述思路有所不同。具体而言,本设计利用长条形磁铁吸引磁流体,形成表针结构,并通过旋转磁铁来控制磁流体的转动,从而实现时间显示功能,其外观如图四所示。此外,该时钟还具备扩展功能,能够切换模式显示温度、湿度和噪声强度等环境信息。通过这种设计,不仅能够提供更多的信息,还在结构上进行了优化,显著降低了制造成本,预计总成本不超过500元。
图四 项目外观设计
项目亮点
- 电机闭环驱动磁铁控制磁流体:基于A4988电机驱动芯片+ADA4571磁场传感器芯片,实现了步进电机闭环驱动,通过控制永磁体的角度,精准控制磁流体的流动。
- 基于嵌入式平台的混沌系统数值仿真:利用C语言实现基于四阶龙格-库塔法的双摆和Lorenz吸引子两个混沌系统的数值仿真,并在OLED屏幕上展示动态仿真过程。
- 锂电池充放电管理及 power path 功能:实现了基于TP4056芯片的锂电池充放电管理功能。支持Type-C充电。插入充电器时,设备直接从Type-C取电;拔出充电器后,自动切换至锂电池供电。
- 模块化设计:项目采用模块化设计。装置由8块PCB和4块亚克力板构成,具备未来扩展和功能升级的灵活性。
系统组成
装置的机械结构一共包括七层(如图五所示),包括8块PCB,四块亚克力板。
图五 装置的机械结构,绿色方框表示该部分为一块PCB,图中未标出固定电机用的亚克力板
装置电气连接示意图如图六所示。装置上有四种功能的电路板。
- 前面板位于磁流体容器前,主控是一个STM32G030F6P6芯片。上面包括作为表盘的刻度的八个数码管,在第三块电路板上还有一个0.96英寸的OLED显示屏。
- 电机驱动板位于磁流体容器之后,主控是一个STM32G030F6P6芯片。上面包括一个A4988步进电机驱动模块,用于驱动一个两相四线步进电机,此外,还有一个用于测量转角的ADA4571芯片以实现步进电机闭环位置控制。
- 核心板位于电机驱动板之后,上面有一个STM32F407VET6芯片作为主控,还有一个ESP32-S3芯片用于实现音频功能。电路板上有一个AHT20传感器,用于测量环境的温度和湿度,以及一个PCF8563模块,这是一个RTC模块,用于获取当前时间。
- 电源板负责为上述所有电路板提供不同电压的供电。电路板借助一个 Type - C 接口与外接电源相连接。电流首先经由一个 TP4056 芯片对锂电池进行充电操作,而后利用一个 MOS 切换供电路径。紧接着,先通过一个 9V 升压模块将电压提升至 9V,再分别经过两个降压模块获取 5V 和 3.3V 电压。如此一来,便能够为其他电路板提供 9V、5V 和 3.3V 这三种不同的电压,以满足不同电路板的工作需求。
图六 装置的电气连接示意图
该装置采用模块化设计,图中所有 PCB 板框层形状相同,故可堆叠并用铜柱连接。这样设计的一个优点是,当期望更新某一模块的功能时,仅需更换装置的一块电路板便可达成目的。这种设计方式极大地提高了装置的可维护性和灵活性,为系统的持续优化和升级提供了便利条件。装置的俯视图如图七。
图七 装置俯视图
项目设计思路及实现方法
磁流体力学特性探究
磁流体选择的是煤油基磁流体,购买自淘宝,每瓶中装有2mL磁流体,如图八所示。
图八 磁流体外观
首先,为了驱动磁流体,我进行了两种不同的尝试。第一种尝试是使用电感作为电磁铁来吸引磁流体,第二种则是使用永磁体来吸引磁流体。在第一种尝试中,我测试了不同规格的工字电感,包括30mH、47mH和100mH,并通入了1至3安培不等的电流,试图通过靠近瓶身来吸引磁流体。然而,无论电感的大小或电流的强度如何,均未能对磁流体产生显著的吸引效果。考虑到换用电磁铁、通入更大的电流可能给装置带来更大的功耗和发热,我决定改用N52钕铁硼永磁体来吸引磁流体。为了给后续的结构设计提供参考,我首先测试了磁铁与瓶身间距对磁流体吸引效果的影响,测量结果如图九所示:
图九 磁铁在不同距离下吸引磁流体效果
经过观察,我发现当磁铁与瓶身之间的距离保持在8毫米以内时,磁铁能够有效地将磁流体吸引至其正对位置。基于这一发现,在项目实施过程中,我将确保磁铁始终位于瓶身8毫米以内的范围内。
步进电机闭环驱动
在项目中,可供选择的电机种类有很多,比如直流有刷电机、直流无刷电机、步进电机以及舵机等等。然而,鉴于项目的具体需求,主要期望实现精准的角度控制,易于驱动,同时还需具备连续旋转的能力,但是对电机的带载能力要求并不高,故而决定选用步进电机。
步进电机作为一种能够将电脉冲信号转化为机械角位移的电动机,其工作原理是通过接收来自控制器的脉冲信号来逐步实现转动(见图十)。每一个脉冲信号都对应着电机轴的一个固定角度旋转,该角度被称作步距角。也正是凭借其精确的定位能力以及简便的控制方式,步进电机在众多需要精确定位的应用场景中获得了广泛的运用。
通常情况下,步进电机主要由定子和转子两部分构成。定子上配备有多相绕组,而转子则是由永磁体或者软磁材料制作而成。当定子绕组按照特定的顺序通电时,便会产生磁场,此磁场与转子上的磁场相互作用,进而促使转子逐步旋转。
图十 步进电机原理,图片来自维基百科
项目中选用了一款小型的两相四线步进电机。该步进电机有多种驱动方式,其中包括:
- MOS管控制驱动:通过使用MOS管(金属氧化物半导体场效应晶体管)来控制施加在每根线上的电压。这种驱动方式适用于需要大功率驱动的场景,可以提供较高的电流和电压控制能力。
- 集成步进电机驱动芯片驱动:直接选用集成的步进电机驱动芯片来驱动电机。这种驱动方式适用于较低功率的场景,通常集成度高、体积小、功耗低,且易于集成到系统中。
考虑到本项目中对体积要求比较严格,而步进电机的负载并不大,因此决定使用A4988步进电机驱动芯片驱动步进电机。A4988是一款集成度高、功能强大的步进电机驱动芯片,由Allegro MicroSystems公司生产,广泛应用于3D打印机、CNC机床、机器人等需要精确控制步进电机的场景。它支持通过外部电阻设置电机电流,并具备过热保护和短路保护功能。
有了该芯片,就只需要用一个IO引脚的电平控制电机转动方向(称为DIR引脚),再用另一个引脚发送脉冲驱动电机旋转即可(称为STEP引脚)。当STEP引脚发送一个脉冲时,步进电机即旋转一个步进角。
驱动步进电机旋转一个步进角的STM32端程序如下:
/**
* 控制电机旋转一个步长
*
* @param direction 旋转方向,FORWARD 表示顺时针,BACKWARD 表示逆时针
* @param angle 旋转角度,单位为度
*
* @return 无返回值
*/
void rotate_one_step(uint8_t direction, uint8_t angle)
{
// 根据旋转方向设置电机的方向引脚
if (direction == FORWARD)
{
// 设置电机方向为顺时针
HAL_GPIO_WritePin(GPIOA, DIR_Pin, GPIO_PIN_RESET);
// 切换步进引脚的状态
HAL_GPIO_TogglePin(GPIOA, STEP_Pin);
}
else
{
// 设置电机方向为逆时针
HAL_GPIO_WritePin(GPIOA, DIR_Pin, GPIO_PIN_SET);
// 切换步进引脚的状态
HAL_GPIO_TogglePin(GPIOA, STEP_Pin);
}
}
步进电机本身是开环驱动的,为了实现更加精准的旋转角度控制,我选择了ADA4571芯片用于测量电机旋转角度。ADA4571 是一款各向异性磁阻(AMR)传感器,集成信号调理放大器和 ADC 驱动器等,在单个封装内有 AMR 传感器和仪表放大器,提供与电源电压成比例的余弦和正弦输出信号。其传感器有特殊电桥结构,具有 180° 角度高精度、热漂移与寿命漂移小等特点。因此只需要在步进电机后端粘贴一个小型径向充磁的磁铁,再使用ADC引脚测量正弦和预先输出信号即可计算出电机的角度。值得注意的是,电机轴承旋转一圈,电角度旋转720度,因此需要对测量出的电角度进行一次换算。
计算角度的STM32函数如下:
/**
* 获取当前角度的函数。
*
* 该函数通过读取 ADC 的值来计算当前的角度。它首先启动 ADC 转换,然后读取正弦和余弦值,
* 并使用反正切函数计算出原始角度。接着,根据映射模式对原始角度进行调整,得到最终的角度值。
* 如果当前角度与真实角度的差大于 10 度,则切换映射模式。
*
* @return 返回计算得到的当前角度。
*/
float get_angle() {
// 启动 ADC 转换
HAL_ADC_Start(&hadc1);
// 等待转换完成
HAL_ADC_PollForConversion(&hadc1, HAL_MAX_DELAY);
// 获取正弦值
VSIN = HAL_ADC_GetValue(&hadc1);
// 再次启动 ADC 转换
HAL_ADC_Start(&hadc1);
// 等待转换完成
HAL_ADC_PollForConversion(&hadc1, HAL_MAX_DELAY);
// 获取余弦值
VCOS = HAL_ADC_GetValue(&hadc1);
// 停止 ADC 转换
HAL_ADC_Stop(&hadc1);
// 计算原始角度
raw_angle = (float)(fxpt_atan2(((int)VSIN - 2048) << 4, ((int)VCOS - 2048) << 4)) / 1491291;
// 根据映射模式计算临时角度
float temp_angle = map_mode == 0? (0.5 * raw_angle + 90) : (0.5 * raw_angle - 90);
// 如果临时角度与真实角度差大于 10 度,切换映射模式
if (fabs(temp_angle - true_angle) > 10)
map_mode = 1 - map_mode;
// 根据映射模式计算真实角度
true_angle = map_mode == 0? (0.5 * raw_angle + 90) : (0.5 * raw_angle - 90);
// 返回真实角度
return true_angle;
}
测量角度的过程中,需要反复调用atan2()
函数,为提高计算速度,这里使用了CORDIC算法来计算atan2()
函数。Coordinate Rotation Digital Computer (CORDIC) algorithm 是一种用于计算三角函数和双曲函数的迭代算法。它最初是为了在没有浮点运算硬件支持的计算机系统中实现这些函数而设计的。想要了解更多可以阅读ST给出的相关资料。该算法具有如下特点:
- 复杂度低:仅使用移位和加法运算,无需浮点运算,也不需要浮点处理单元(FPU)或数字信号处理器(DSP),适合如 Cortex-M0 这样的内核。
- 内存占用极小:仅需一个小型查找表(LUT),表中整数条目数量与所需精度位数相同,相比之下,ARM CMSIS 库占用 220kB,而 CORDIC 可能不到 1kB。
- 速度快:平均执行的迭代次数等于所需精度位数。相对于平均耗时为常量时间(T)的单精度 FPU,ARM CMSIS 可能更快(T 到 T/3),CORDIC 可能稍慢(T 到 3T),而软件模拟单 / 双精度浮点运算则是最慢的解决方案(10T 到 30T)。它可以计算多种三角函数如 sin、cos、atan、atan2,以及双曲函数如 sinh、cosh、atanh 等,还能作为副产品计算出 sqrt、exp、ln 等函数,并且总是成对计算函数。
CORDIC 算法通过一系列的旋转或拉伸操作来逼近所需的三角函数值。该算法的基本思想是将复杂的三角函数或双曲函数转换为一系列的简单旋转,这些旋转可以通过定点数的算术运算来实现。通过这种方式,CORDIC 算法可以在不使用浮点运算的情况下,使用整数运算来逼近三角函数的值。基于该原理的atan2()
函数实现如下:
int CORDIC_ZTBL[] = {0x04000000, 0x025C80A4, 0x013F670B, 0x00A2223B,
0x005161A8, 0x0028BAFC, 0x00145EC4, 0x000A2F8B, 0x000517CA, 0x00028BE6,
0x000145F3, 0x0000A2FA, 0x0000517D, 0x000028BE, 0x0000145F, 0x00000A30};
// 定义整数数组 CORDIC_ZTBL,用于 CORDIC 算法
int fxpt_atan2(int y, int x)
{
int k, tx, z = 0, fl = 0;
if (x < 0)
{
fl = ((y > 0)? +1 : -1);
x = -x;
y = -y;
}
for (k = 0; k < CORDIC_MAXITER; k++)
{
tx = x;
if (y <= 0)
{
x -= (y >> k);
y += (tx >> k);
z -= CORDIC_ZTBL[k];
}
else
{
x += (y >> k);
y -= (tx >> k);
z += CORDIC_ZTBL[k];
}
}
if (fl!= 0)
{
z += fl * CORDIC_PI;
}
return z;
}
得到了测量电机角度和驱动电机步进的函数,便可以得到闭环控制电机旋转的函数。此外项目中还实现了对转动方向的精细调整,在温度、湿度、噪声强度模式下,电机被控制不得跨越6点钟方向旋转。以上所有功能使用定时器实现,定时器的回调函数如下:
/**
* 计算两个角度之间的差异,并确保差值在0到180度之间。
*
* @param angle1 第一个角度
* @param angle2 第二个角度
* @return 两个角度之间的差异,范围在0到180度之间
*/
float angle_diff(float angle1, float angle2)
{
// 使用fmod函数计算两个角度的差,并将结果限制在0到360度之间
float diff = fmod(angle1 - angle2 + 360, 360);
// 如果差值大于180度,则取其补角,使得差值在0到180度之间
if (diff > 180)
diff = 360 - diff;
// 返回最终的角度差值
return diff;
}
/**
* 定时器周期性中断回调函数
*
* @param htim 定时器句柄
*/
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if (htim->Instance == TIM14)
{
float theta = get_angle(); // 获取当前角度
float target = target_angle; // 获取目标角度
// 更新数据但不在中断中发送
snprintf(buffer, sizeof(buffer), "VSIN: %d, VCOS: %d, theta: %f, target:%f\r\n", VSIN, VCOS, theta, target);
uart_flag = 1; // 设置标志位,通知主循环发送数据
//HAL_UART_Transmit(&huart1, (uint8_t *)buffer, strlen(buffer), HAL_MAX_DELAY);
if (angle_diff(theta, target) < 1)
{
return; // 如果当前角度与目标角度差异小于1度,则直接返回
}
if (cross_en == 0)
{
if (target > theta)
{
rotate_one_step(FORWARD, target - theta); // 向前旋转一步
}
else
{
rotate_one_step(BACKWARD, theta - target); // 向后旋转一步
}
}
else
{
float pos_delta = target - theta; // 计算目标角度与当前角度的差值
if (pos_delta < 0)
pos_delta += 360; // 如果差值为负,则加上360度
if (pos_delta < 180)
{
rotate_one_step(FORWARD, pos_delta); // 向前旋转一步
}
else
{
rotate_one_step(BACKWARD, 360 - pos_delta); // 向后旋转一步
}
}
}
}
最终实测电机可以实现小于1度误差的闭环控制精度,基本上达到了该步进电机的精度上限和传感器的上限。效果如图十一:
图十一 装置在时钟模式和环境模式下切换
此外,开发至此,还遇到一个棘手的问题。之前已经设计好了前面板的PCB,每一个前面板都需要一个UART接口用于和主控板通信,那么如果电机驱动板采用同样的通信方案,主控板将需要7个串口接口,超过了STM32F407VET6的串口数量(6个)。因此,这里实现了一个串口转发的小设计。电机驱动板上的STM32G030F6P6芯片有两个USART接口,一个用于接口信息,而另一个用于将接收到的信息立刻转发给下一个电机驱动板。这样只需要核心板一次性将驱动三个电机的所有信息全部通过一个串口发送给第一个电机驱动板就能实现三个电机的驱动了。下面是串口的DMA接受中断回调函数:
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
if (huart->Instance == USART1)
{
target_angle = ((float)DATA_BUFF[MOTOR_ID] - OFFSET) * SCALE_FACTOR;
cross_en = DATA_BUFF[3];
test_mode = DATA_BUFF[4];
HAL_UART_Transmit(&huart2, DATA_BUFF, 5, HAL_MAX_DELAY);
}
}
混沌系统数值仿真
这一部分实现了对双摆和Lorenz吸引子进行数值仿真并将其轨迹显示在OLED屏幕上的功能。双摆是一个经典的力学系统,由两个可以自由转动的摆构成。这两个摆通过一根不可伸长的细杆连接。Lorenz吸引子是一个著名的混沌系统,由气象学家Edward Lorenz在1963年发现。该系统描述了大气对流中的一些现象,如天气变化。这两个系统的共同点就是塔恩都是一个混沌系统,即给定一个初始状态,系统的长期行为无法预测,如果对初始状态施加微弱的扰动,将会对未来的状态产生巨大影响,即人们所熟知的“蝴蝶效应”。 两个系统的仿真结果见图十二。
图十二 双摆的仿真结果(左)Lorenz吸引子的仿真结果(右)
在本项目中引入这一功能的初衷主要是期望给 OLED 增添一个动画效果。然而,需注意的是,STM32G030F6P6 单片机的 SRAM 容量仅为 8KBytes,Flash 容量也仅有 32KBytes,在这样有限的存储条件下,要保存为时钟模式和环境模式所设计的两段动画存在一定难度。鉴于此,有必要深入研究一种基于方程生成的动画。然而,传统基于方程的动画通常具有较强的重复性,变化相对匮乏,而双摆和Lorenz吸引子两个混沌系统自身具有丰富的变化性,其特点是有界但无循环。基于此,经过综合考量,决定不在单片机上预先保存好动画的每一帧信息,而是在STM32G030F6P6单片机上对双摆和 Lorenz 吸引子进行实时数值仿真,并将其运动轨迹在 OLED 屏幕上予以显示。双摆的动画和时钟上表针的运动有诸多共同点,因此将会在时钟模式下播放。Lorenz吸引子诞生于科学家对大气流动的研究,因此将会在环境模式下播放。这样的设计旨在充分利用混沌系统的特性,为动画效果带来更多的变化和创新性,同时也能在有限的硬件资源条件下,尽可能地实现预期的功能目标。
龙格-库塔法(Runge-Kutta Method)
龙格-库塔法(Runge-Kutta Method)是一种用于求解常微分方程(ODEs)的数值方法。它是由德国数学家卡尔·龙格(Carl Runge)和马丁·威廉·库塔(Martin Wilhelm Kutta)在20世纪初独立发展出来的。龙格-库塔法因其高精度和广泛的应用而成为数值分析中的一个重要工具。龙格-库塔法的基本思想是通过在每个时间步长内使用多个评估点(称为“阶段”)来提高数值解的精度。这些评估点用于估计导数的平均值,从而更准确地预测下一个时间点的解。最常见的龙格-库塔法是四阶龙格-库塔法(RK4),它使用四个评估点来计算每个时间步长的解。RK4方法的步骤如下:
假设我们有一个一阶常微分方程,,则RK4方法的步骤如下:
首先计算更新值:
然后更新解:
其中, 是时间步长, 是当前时间点, 是当前解, 是下一个时间点的解。
优点
1. 高精度:RK4方法的局部截断误差为 ,全局截断误差为 ,这使得它在许多应用中非常精确。
2. 适用性广:适用于各种类型的常微分方程,包括非线性和刚性方程。
缺点
1. 计算量大:每个时间步长需要计算四次函数值,这在某些情况下可能会导致计算成本较高。不过这个计算量对于主频16MHz的单片机来讲依然可以实现较高刷新率的更新。
2. 不适用于隐式方程:龙格-库塔法通常是显式的,对于某些隐式方程可能需要其他方法。因此需要选择能使用显式微分常微分方程组表示的系统进行数值仿真。
双摆系统
双摆是将一根单摆连接在另一个单摆的尾部所构成的系统。双摆同时拥有着简单的构造和复杂的行为。高能量双摆的摆动轨迹表现出对于初始状态的极端敏感。两个初始状态差异极小的双摆在一段时间的运行后表现非常不同,是一种具有混沌性质的简单动力系统。
双摆系统由两个质量点组成,每个质量点通过无质量的刚性杆连接到固定点和一个质量点。系统的动力学行为由四个变量描述:两个角度 和 ,以及两个角动量 和 。
变量定义:
- :第一个摆的角度。
- :第二个摆的角度。
- :第一个摆的角动量。
- :第二个摆的角动量。
参数定义:
- :摆的质量(假设两个摆的质量相同)。
- :摆的长度(假设两个摆的长度相同)。
- :重力加速度。
微分方程组:
其中,
有了数学表达式我们就可以在单片机上使用C语言进行实现了,这里使用了一个循环队列来保存第二个摆杆末端的轨迹。
/**
* @brief 定义双摆系统微分方程
* @param 双摆的状态变量y,待写入的双摆的状态变量变化量dy指针
* @retval 无
*/
void f(double_pendulum_State y, double_pendulum_State *dy)
{
double th1 = y.theta1;
double th2 = y.theta2;
double pth1 = y.pth1;
double pth2 = y.pth2;
double m = 1;
double l = 1;
double g = 9.8;
double cos_diff = cos(th1 - th2);
double sin_diff = sin(th1 - th2);
double M = l * l * m * (-16 + 9 * cos_diff * cos_diff);
double dth1 = -6 * (2 * pth1 - 3 * pth2 * cos_diff) / M;
double dth2 = -6 * (8 * pth2 - 3 * pth1 * cos_diff) / M;
double dpth1 = -0.5 * l * m * (3 * g * sin(th1) + dth1 * dth2 * l * sin_diff);
double dpth2 = 0.5 * l * m * (dth1 * dth2 * l * sin_diff - g * sin(th2));
dy->theta1 = dth1;
dy->theta2 = dth2;
dy->pth1 = dpth1;
dy->pth2 = dpth2;
}
/**
* @brief 更新双摆系统的状态并在OLED 屏幕上显示
* @param 无
* @retval 无
*/
void update_double_pendulum()
{
f(double_pendulum_y, &k1);
// 计算 k2
temp.theta1 = double_pendulum_y.theta1 + DT / 2 * k1.theta1;
temp.theta2 = double_pendulum_y.theta2 + DT / 2 * k1.theta2;
temp.pth1 = double_pendulum_y.pth1 + DT / 2 * k1.pth1;
temp.pth2 = double_pendulum_y.pth2 + DT / 2 * k1.pth2;
f(temp, &k2);
// 计算 k3
temp.theta1 = double_pendulum_y.theta1 + DT / 2 * k2.theta1;
temp.theta2 = double_pendulum_y.theta2 + DT / 2 * k2.theta2;
temp.pth1 = double_pendulum_y.pth1 + DT / 2 * k2.pth1;
temp.pth2 = double_pendulum_y.pth2 + DT / 2 * k2.pth2;
f(temp, &k3);
// 计算 k4
temp.theta1 = double_pendulum_y.theta1 + DT * k3.theta1;
temp.theta2 = double_pendulum_y.theta2 + DT * k3.theta2;
temp.pth1 = double_pendulum_y.pth1 + DT * k3.pth1;
temp.pth2 = double_pendulum_y.pth2 + DT * k3.pth2;
f(temp, &k4);
// 更新状态
double_pendulum_y.theta1 += DT / 6 * (k1.theta1 + 2 * k2.theta1 + 2 * k3.theta1 + k4.theta1);
double_pendulum_y.theta2 += DT / 6 * (k1.theta2 + 2 * k2.theta2 + 2 * k3.theta2 + k4.theta2);
double_pendulum_y.pth1 += DT / 6 * (k1.pth1 + 2 * k2.pth1 + 2 * k3.pth1 + k4.pth1);
double_pendulum_y.pth2 += DT / 6 * (k1.pth2 + 2 * k2.pth2 + 2 * k3.pth2 + k4.pth2);
// 在OLED屏幕上绘制双摆及末端轨迹
uint8_t x0 = 64, y0 = 32;
uint8_t x1 = x0 + sin(double_pendulum_y.theta1) * 15, y1 = y0 + cos(double_pendulum_y.theta1) * 15;
x1 = x1 > 127 ? 127 : x1;
y1 = y1 > 63 ? 63 : y1;
uint8_t x2 = x1 + sin(double_pendulum_y.theta2) * 15, y2 = y1 + cos(double_pendulum_y.theta2) * 15;
x2 = x2 > 127 ? 127 : x2;
y2 = y2 > 63 ? 63 : y2;
OLED_Clear();
OLED_DrawLine(x0, y0, x1, y1);
OLED_DrawLine(x1, y1, x2, y2);
push(&q, (point){x2, y2});
for (int i = 0; i < 10; i++)
{
OLED_DrawPoint(q.items[(q.tail - i + QUEUE_SIZE) % QUEUE_SIZE].x, q.items[(q.tail - i + QUEUE_SIZE) % QUEUE_SIZE].y);
}
OLED_Update();
}
最终实现效果如图十三:
图十三 双摆系统动画效果
Lorenz 吸引子系统
Lorenz吸引子系统是由美国气象学家爱德华·洛伦兹(Edward Lorenz)在1963年提出的一个非线性动力系统。这个系统最初是为了模拟大气对流而开发的,但它后来成为了混沌理论中的一个经典例子,展示了复杂系统如何表现出不可预测的行为。Lorenz吸引子系统由以下三个耦合的非线性常微分方程描述:
其中
- ,, 是系统的状态变量。
- 是普朗克数, 是瑞利数,是系统的参数。
常见的参数值为
Lorenz吸引子系统表现出典型的混沌行为,即对初始条件的极端敏感性。即使初始条件有微小的差异,系统在长时间内的行为也会变得完全不同。这种特性使得系统的长期预测变得非常困难。Lorenz吸引子是一个奇异吸引子,具有分形结构。它在三维空间中形成一个类似蝴蝶翅膀的形状,由两个旋转的螺旋结构组成。系统的状态轨迹会在这些螺旋结构之间来回跳跃,但永远不会重复。
有了数学表达式我们就可以在单片机上使用C语言进行实现了,这里使用了一个循环队列来保存运动点在平面的轨迹。具体实现代码如下:
/**
* @brief 定义Lorenz吸引子系统微分方程
* @param 系统的状态变量y,待写入的系统的状态变量变化量dy指针
* @retval 无
*/
void lorenz_system_derivative(LorenzSystem y, LorenzSystem *dy)
{
double sigma = 10.0;
double r = 28.0;
double beta = 8.0 / 3.0;
dy->dx = sigma * (y.y - y.x);
dy->dy = r * y.x - y.y - y.x * y.z;
dy->dz = -beta * y.z + y.x * y.y;
}
/**
* @brief 更新Lorenz吸引子系统的状态并在OLED 屏幕上显示
* @param 无
* @retval 无
*/
void update_lorenz_system()
{
double dt = 0.03;
LorenzSystem k1, k2, k3, k4, temp;
// 计算 k1
lorenz_system_derivative(Lorenz_y, &k1);
// 计算 k2
temp.x = Lorenz_y.x + 0.5 * dt * k1.dx;
temp.y = Lorenz_y.y + 0.5 * dt * k1.dy;
temp.z = Lorenz_y.z + 0.5 * dt * k1.dz;
lorenz_system_derivative(temp, &k2);
// 计算 k3
temp.x = Lorenz_y.x + 0.5 * dt * k2.dx;
temp.y = Lorenz_y.y + 0.5 * dt * k2.dy;
temp.z = Lorenz_y.z + 0.5 * dt * k2.dz;
lorenz_system_derivative(temp, &k3);
// 计算 k4
temp.x = Lorenz_y.x + dt * k3.dx;
temp.y = Lorenz_y.y + dt * k3.dy;
temp.z = Lorenz_y.z + dt * k3.dz;
lorenz_system_derivative(temp, &k4);
// 更新状态
Lorenz_y.x += (dt / 6.0) * (k1.dx + 2.0 * k2.dx + 2.0 * k3.dx + k4.dx);
Lorenz_y.y += (dt / 6.0) * (k1.dy + 2.0 * k2.dy + 2.0 * k3.dy + k4.dy);
Lorenz_y.z += (dt / 6.0) * (k1.dz + 2.0 * k2.dz + 2.0 * k3.dz + k4.dz);
OLED_Clear();
uint8_t x0 = Lorenz_y.x * 3.2 + 64, y0 = Lorenz_y.z;
x0 = x0 = x0 > 127 ? 127 : x0;
y0 = y0 > 63 ? 63 : y0;
push(&Lorenz_q, (point){x0, y0});
for (int i = 0; i < Lorenz_q.count; i++)
{
OLED_DrawPoint(Lorenz_q.items[i % QUEUE_SIZE].x, Lorenz_q.items[i % QUEUE_SIZE].y);
}
OLED_Update();
}
最终实现的效果如图十四所示:
图十四 Lorenz吸引子系统动画效果
PCB设计及软件框架设计
所有PCB采用统一的外形设计,并通过铜柱连接实现堆叠,此举不仅增强了结构强度,还为后续功能升级提供了尺寸基准,同时实现了功能模块的解耦,从而降低了设计错误带来的损失。图十五从左至右依次展示了电机驱动板、电源板、核心板及前面板的EDA生成的三维模型。
图十五 项目中设计的电路板外观
核心板上,为了便于调试还包括了基于CP2102的ESP32-S3的串口烧写电路。只需插入一根micro-USB线即可对ESP32进行编程调试。这一功能电路原理图如图十六所示:
图十六 核心板电路原理图,ESP32相关部分
电源板上设计了自动切换供电的功能,当插入type-C线缆时,电路中MOS管截止,充电的同时由外接电源为整个装置供电。拔出充电线之后,MOS管导通,由电池为装置供电。由于电流进入装置前首先要经过升压模块,模块内部以及装置哥哥板子上都有维持电压的电容,所以这个切换过程不会引起装置掉电。同时,还将Type-C引脚中CC引脚拉低,理论上能够提供5V3A的供电能力。电路原理图如图十七所示:
图十七 电源板电路图 充电部分
前面板上主要为一组数码管驱动电路,加上一组OLED驱动电路。项目中,数码管用于显示表盘的刻度。单片机的IO数量和驱动能力都不足以驱动8个数码管,这里选择了MAX7219芯片驱动数码管。MAX7219数码管驱动芯片是美信公司生产的具有24引脚的串行输入/输出共阴极芯片,通过4线串行接口与微处理器相连,能驱动8个7段数字LED等,具备多种译码方式,可调节亮度、设置扫描范围,有低功耗模式且支持级联。数码管驱动电路和OLED驱动电路如图十八所示:
图十八 前面板电路原理图 数码管驱动和OLED驱动部分
四块电路板及其程序都会开源在电子森林本页面最下方,感兴趣的小伙伴们可以动手试一试。
程序框架方面,为了实现整个流程,项目中设计了如图十九所示程序框架:
图十九 项目整体软件流程图
附1:设计中用到的指定厂商元器件及介绍
STM32F407VET6
STM32F407VET6 是意法半导体(STMicroelectronics)推出的一款高性能的32位微控制器。以下是其主要特点:
1. 强大的内核性能:采用 ARM Cortex-M4 内核,主频高达 168MHz,具有较高的运算处理能力。该内核拥有单周期乘法和硬件除法运算能力,支持 DSP 指令集,并具备浮点运算单元,能够高效地处理复杂的数字信号处理任务和浮点运算。
2. 丰富的存储资源:片内集成了高达 512KB 的闪存,用于存储程序代码和数据;还有 192KB 的 SRAM,可用于存储临时数据,满足各种应用程序的存储需求。
3. 多样的外设接口:
- 具备丰富的通信接口,包括4个 UART(通用异步收发传输器)、2个USART(通用同步异步收发传输器)、3个SPI(串行外设接口)、3个I2C(内部集成电路)等,方便与各种外部设备进行通信。
- 拥有 3 个 12 位的 ADC(模数转换器),最多可支持 24 个通道,转换速度可达 2.4Msps,能够精确地采集模拟信号;还有 2 个 12 位的 DAC(数模转换器),可用于输出模拟信号。
- 包含多个定时器,如多达 12 个 16 位定时器和 2 个 32 位定时器,可满足不同的定时需求,其中部分定时器还可用于电机控制等特定应用。
- 配备了一个真随机数发生器(RNG),为需要随机数的应用提供了便利。
- 具有以太网 MAC 和用于 CMOS 传感器的照相机接口,适用于网络通信和图像采集等应用。
4. 灵活的电源管理:支持 1.8V 到 3.6V 的电源供电,具有多种电源管理模式,包括睡眠、停止和待机模式等,可有效降低功耗,满足不同应用场景下的低功耗需求。
5. 封装与引脚:采用 LQFP100 封装,具有 100 个引脚,方便在电路板上进行焊接和连接。
项目中选择STM32F407VET6的主要考虑是,作为核心控制器,该MCU必须要拥有引脚不冲突的5个UART接口,两个I2C接口(当然这个可以使用同一个I2C总线驱动两个I2C外设),因此选择了这款外设数量较多的MCU。
STM32G030F6P6
STM32G030F6P6 是意法半导体(STMicroelectronics)推出的一款低功耗32位微控制器。其核心特点如下:
- 核心性能:基于高性能 Arm Cortex -M0+ 32 位 RISC 内核,运行频率高达 64MHz。适用于消费、工业和家电领域的各种应用,为物联网解决方案做好准备。
- 集成特性:集成内存保护单元(MPU)、高速嵌入式存储器(8KB SRAM 和高达 64KB 带有读保护和写保护的闪存程序存储器)、DMA、广泛的系统功能、增强型 I/O 和外设。
- 功能接口:提供标准通信接口(两个 I2C、两个 SPI/一个 I2S 和两个 USART)、一个 12 位 ADC(2.5MSps,最多 19 个通道)、低功耗 RTC、高级控制 PWM 定时器、四个通用 16 位定时器、两个看门狗定时器和一个 SysTick 定时器。
- 价格:目前这款MCU仅售价两元左右!
选取这款MCU的主要目的其实就是看中了这个MCU在价格特别低的同时还拥有两个USART、一个ADC、两个I2C接口。项目中的电机驱动板、前面板共计需要6个MCU,选择这个MCU极大地降低了成本。同时,这款MCU只需要极少的外围元件就能驱动起来(本项目中甚至没有使用外接晶振,而是使用芯片内部振荡器就实现了所有功能)。
ESP32-S3-WROOM-1
ESP32-S3-WROOM-1是乐鑫公司推出的一款通用型 Wi-Fi + 低功耗蓝牙 MCU 模组,搭载 ESP32-S3 系列芯片。具有丰富外设接口、强大神经网络运算能力和信号处理能力,适用于 AloT 领域多种应用场景,如唤醒词检测和语音命令识别、人脸检测和识别、智能家居、智能家电、智能控制面板、智能扬声器等。模组采用 ESP32-S3 系列芯片,该芯片具有以下特点:
- 性能强劲:搭载 Xtensa®32 位 LX7 双核处理器,工作频率高达 240MHz,支持单精度浮点运算单元,且 CPU 电源可关闭,利用低功耗协处理器监测外设状态变化或模拟量是否超出阈值。
- 外设丰富:集成丰富外设,包括模组接口如 SPI、LCD、Camera 接口、UART、I2C、I2S、红外遥控、脉冲计数器、LED PWM、USB Serial/JTAG 控制器、MCPWM、SDIO host、GDMA、TWAIN 控制器(兼容 ISO 11898-1)、ADC、触摸传感器、温度传感器、定时器和看门狗,以及多达 45 个 GPIO。
选择这款芯片的主要目的是该芯片带有Wi-Fi和蓝牙接口,同时具备强大的神经网络运算能力和信号处理能力,能够实现语音识别等功能,这些功能为本项目未来功能的拓展提供了广阔的空间。
ADA4571
ADA4571是ADI公司推出的一款集成的各向异性磁阻(AMR)角度传感器和信号调理器,由Analog Devices生产。该芯片具有以下主要特性和应用:
- 高精度180°角度传感器:最大角度误差为0.5°,提供模拟正弦和余弦输出,以及比率计量输出电压。
- 低热漂和寿命漂移:在-40°C至+150°C的温度范围内工作,具有低热漂和寿命漂移,适合长期稳定使用。
- 电磁干扰(EMI)抵抗:设计有EMI滤波器,以减少电磁干扰对信号的影响。
- 故障诊断:集成了故障诊断功能,可以自我检查传感器和IC条件。
- 应用领域:适用于绝对位置测量(线性和角度)、无刷直流电机控制和定位、执行器控制和定位、无接触角度测量和检测、磁性角度位置感应等。
- 单芯片解决方案:ADA4571包含两个芯片,一个AMR传感器和一个固定增益(G = 40名义上)的仪表放大器,提供干净、放大的与旋转磁场角度相关的余弦和正弦输出信号。
- 温度补偿模式:传感器包含两个惠斯通电桥,相对角度为45°,可以在旋转磁场中提供两个正弦输出信号,其频率是传感器与磁场方向之间角度的两倍。
这是一款FastBond第一季活动中两位小伙伴分享过的使用方案。我从电子森林分享的项目中看到了过去有小伙伴利用这款芯片实现了非常好的角度测量效果,正好这款芯片完美符合本项目的需要,因此就选择了这款芯片用于步进电机转角测量。
附2:对本大赛的心得体会(包括意见或建议)
项目从七月份开始推出时就开始着手做。本来预期在开学之前完成,但期间遇到了几次芯片选型的问题和电路板绘制的问题,也曾忙于学校和家里的事情而暂时放下,终于在十月底赶在DDL前实现了预期的大部分功能。其中最大的体会是嵌入式开发是可谓是“正入万山圈子里,一山放过一山拦”,面对层出不穷的问题,只能尽己所能推进进度,留出时间余量来应对可能出现的各种情况。每一个小的功能点的实现都会给我带来莫大的成就感,每一个故障都会让本就模糊的规划更难掌控。不论发生什么,最重要的还是打起精神抓起烙铁,投入到无尽的开发中去。:-)