两轮自平衡小车
用陀螺仪和加速度计做姿态解算,直立环与速度环串起来控制。目前能稳定直立,速度环还在调。
- I²C 传感器
- 互补滤波
- 定时器 PWM
- 串级 PID
这里是我课余整理的技术笔记:从 PCB 画板与打样、单片机外设与定时器,到 运动解算 与 闭环 PID 控制。 能画出来的就画成图,能推导的就把公式写清楚,能动手验证的就写个仿真或做个实物。
顺序不是绝对的,但每往前一步,都会让后面少踩一些坑。比如先懂器件和电源,画板时才知道为什么要放去耦电容;先让单片机跑起来,才谈得上采样和控制。
看懂电阻电容电感、三极管与运放的基本用法,会用万用表测通断和电压。
画原理图、做封装、布线与检查 DRC,然后下单打样,等板子的这几天顺便看手册。
点亮 LED 只是开始,重点是 GPIO、定时器、串口、ADC 这些外设背后的时序。
UART / I2C / SPI / CAN,配合逻辑分析仪看波形,学会从波形判断问题出在哪。
从开环到闭环,理解 PID 三项各自在做什么,再学滤波、采样与离散化。
把上面几块拼起来做一个完整的东西:平衡车、循迹小车、小机械臂等等。
原理图只是“谁连谁”,PCB 决定了“电流怎么走”。同一张原理图,两个走线方案可能一个安静稳定,另一个一上电就复位、通信就丢包。
电流永远走回路。信号线从芯片流到器件,再沿着参考平面(通常是 GND 平面)流回芯片。这条回流路径越贴近信号线,环路面积就越小,辐射和干扰也就越小。
所以四层板常见的叠层是信号—地—电源—信号,把两个内层当作完整的参考平面。这也是为什么布线时最怕的是:在平面正下方开了一道长长的槽,把回流的路剪断了。
电源、功率、模拟、数字分区摆放,接口靠近板边,让大电流路径短而宽。
晶振紧贴芯片 XIN/XOUT,走线尽量短、下方不走其他信号线,负载电容按手册选。
采样信号远离时钟与开关电源,尽量差分走线或两侧包地,ADC 参考电压单独滤波。
一个过孔大约 1 nH 电感。高速线上少打孔,换层时旁边补回流地过孔。
外部接口加 TVS / ESD 管、限流电阻与滤波,避免拔插和静电直接打到芯片引脚。
关键电源网络留测试点,串口与 SWD 引出排针,丝印标清引脚定义和版本号。
外设库把寄存器包起来了,但硬件行为没变:定时器怎么计数、PWM 什么时候翻转、中断标志什么时候清,这些决定了程序稳不稳。
定时器本质是一个不断累加的计数器。计数到 ARR(自动重装载值)时归零并产生更新事件;把比较值 CCR 拿出来和计数器比较,就能做到“数到 CCR 之前输出高、之后输出低”,这就是 PWM。
/* 系统时钟 72 MHz,PWM 20 kHz:PSC=0, ARR=3599 */ htim3.Init.Prescaler = 0; htim3.Init.Period = 3599; // ARR HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1); // 改占空比:50% __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, 1800);
volatile uint32_t tick = 0; void TIM2_IRQHandler(void) { if (TIM2->SR & TIM_SR_UIF) { TIM2->SR = ~TIM_SR_UIF; // 先清标志,否则会反复进中断 tick++; // 中断里只做最短的事 } }
HAL_Delay() 或往串口 printf,中断频率一高主循环就再也跑不动。中断只置标志,重活留给主循环。volatile,编译器优化后主循环读到的永远是旧值;共享的多字节变量还要用临界区(关中断)保护。通信调不通的时候先别急着改代码。拿逻辑分析仪抓一段波形,看起始条件、时钟极性、地址位、ACK 位对不对,通常几分钟就能定位。
| 线数 | 常见速率 | 拓扑 | 典型场景 | 最容易踩的坑 | |
|---|---|---|---|---|---|
| UART | 2(TX/RX) | 9.6 k – 1 M | 点对点 | 调试打印、蓝牙/WiFi/GPS 模块 | 波特率误差、不共地、TX/RX 交叉 |
| I²C | 2(SCL/SDA) | 100 k – 400 k | 多主多从(地址寻址) | 传感器、EEPROM、低速外设 | 忘记上拉、地址写错、总线被拉死 |
| SPI | 4(CS/SCK/MOSI/MISO) | 1 M – 50 M | 一主多从(片选) | 屏幕、Flash、高速 ADC | CPOL/CPHA 不匹配、片选时序 |
| CAN | 2(CAN_H/L) | 125 k – 1 M | 多主总线 | 电机驱动器、车载、工业设备 | 终端电阻缺失、ID 冲突、波特率不符 |
运动学就是位置和角度之间的换算。正解是“已知角度求末端位置”,逆解是“已知目标位置求角度”。逆解通常更难,因为可能有多解,也可能无解。
两根连杆、两个旋转关节,是最容易讲清楚的机械臂模型。设第一段长 l₁、第二段长 l₂,关节角为 θ₁、θ₂(相对角),末端坐标就是两个向量相加:
反过来,已知目标点 (x, y) 求角度就是逆解。先用余弦定理求肘关节角:
acos() 的参数必须限制在 −1 ~ 1,否则会返回 NaN;角度与弧度别混用(180° = π rad);atan2 的参数顺序是 (y, x),写反了角度会整体转 90°。两轮差速小车只有线速度 v 和角速度 ω 两个控制输入,但电机只接收左右轮速。中间的换算就是最简单的一次运动解算:
开环控制是“我猜要给多少就给多少”,闭环控制是“我先看结果偏了多少,再决定给多少”。PID 是最常用的修正方式,三项分别对应现在、过去和趋势。
u[k] = Kp·e[k] + Ki·T·Σe[i] + Kd·(e[k]−e[k−1])/T。所以采样周期必须稳定,否则参数就失去意义。typedef struct { float kp, ki, kd, T; float i; // 积分累加项 float prev_e; float out_min, out_max; } pid_t; float pid_step(pid_t *p, float target, float meas) { float e = target - meas; p->i += p->ki * e * p->T; // 积分项 if (p->i > p->out_max) p->i = p->out_max; // 积分限幅,防止饱和 if (p->i < p->out_min) p->i = p->out_min; float d = (e - p->prev_e) / p->T; // 微分(可加低通滤波) p->prev_e = e; float u = p->kp * e + p->i + p->kd * d; if (u > p->out_max) u = p->out_max; if (u < p->out_min) u = p->out_min; return u; }
都是学生阶段的练习项目,目的不是做成产品,而是把上面这些知识串起来用一遍。下面记录的是我在做的过程中学到的部分。
用陀螺仪和加速度计做姿态解算,直立环与速度环串起来控制。目前能稳定直立,速度环还在调。
灰度传感器阵列识别黑线位置,用 PID 修正左右轮速差。直道跑得比较稳,弯道还在调参数。
上面那个逆解演示就是按它的尺寸做的模型。目前能按给定坐标做点到点运动,接下来想试试轨迹插值。
工具不在多,顺手就行。资料方面我的体会是:芯片手册永远比教程可靠,遇到时序问题直接翻参考手册,比搜半天博客快得多。
调通信我用得最多的是逻辑分析仪:协议解码一开,起始位、地址、ACK 全在波形上,比猜快多了。
我是一名在校学生,业余在学嵌入式相关的东西:电路、PCB、单片机、控制算法。这个站点是我自己整理的学习笔记,把平时看资料、做实验、踩坑的过程记下来,顺便画成图方便以后回顾。
它不是教程,更不是商业推广。内容都按我自己的理解写,可能有不准确甚至错误的地方,如果你发现问题,非常欢迎指出,我会去核对后修改。
如果你也在学类似的东西,欢迎一起交流:
除特别注明外,本站笔记采用「署名—非商业性使用—相同方式共享」的方式共享,转载请注明出处。