LZ-SG/embedded-dev-tutorials
0
1#!/usr/bin/env python32# -*- coding: utf-8 -*-3"""批量深度优化嵌入式教程 - 注入高级技术内容"""4import os, re5 6BASE = os.path.dirname(os.path.abspath(__file__))7TUTORIALS = os.path.join(BASE, "static", "tutorials")8 9# ============================================================10# 各教程深度扩展内容(插入到FAQ之前)11# ============================================================12 13DEEP_CONTENT = {14 "proj-pwm-motor.html": """15<h2>⚡ 电机驱动芯片深度对比</h2>16<table>17<tr><th>芯片</th><th>通道</th><th>峰值电流</th><th>Rds(on)</th><th>压降@1A</th><th>PWM频率</th><th>价格</th><th>适用场景</th></tr>18<tr><td>L298N</td><td>2路H桥</td><td>2A</td><td>—(BJT)</td><td>~2.5V</td><td>≤25kHz</td><td>¥5</td><td>教学/低速</td></tr>19<tr><td>TB6612FNG</td><td>2路H桥</td><td>3.2A</td><td>0.5Ω</td><td>~0.5V</td><td>≤100kHz</td><td>¥8</td><td>小型机器人</td></tr>20<tr><td>DRV8833</td><td>2路H桥</td><td>2A</td><td>0.36Ω</td><td>~0.36V</td><td>≤50kHz</td><td>¥6</td><td>低功耗</td></tr>21<tr><td>L9110S</td><td>1路H桥</td><td>0.8A</td><td>—(BJT)</td><td>~2V</td><td>≤10kHz</td><td>¥2</td><td>微型电机</td></tr>22<tr><td>BTN7971B</td><td>半桥×2</td><td>70A</td><td>7mΩ</td><td>~0.007V</td><td>≤25kHz</td><td>¥35</td><td>大功率竞赛</td></tr>23</table>24 25<div class="note">26<strong>为什么L298N效率低?</strong> 它是BJT达林顿管结构,饱和压降Vce(sat)约1.8-2.5V。12V供电时,电机实际得到仅9.5V,效率约79%,其余变成热量。而MOSFET驱动的TB6612 Rds(on)仅0.5Ω,1A时压降仅0.5V,效率约96%。27</div>28 29<h2>🧮 死区时间 (Dead-Time) 详解</h2>30<p>H桥在同一半桥的两个MOSFET/BJT之间切换时,如果控制信号没有延时,会瞬间短路(直通/shoot-through),烧毁驱动芯片。</p>31 32<pre><code>// STM32高级定时器死区插入 - TIM1/TIM8支持33// 死区时间 = DTG[7:0] × Tdtg34// Tdtg = tDTS (当 DTG[7:5]=000), tDTS = 1/72MHz = 13.89ns35 36void TIM1_DeadTime_Config(uint8_t deadtime_ns) {37 // 死区时间 = deadtime_ns / 13.89ns ≈ deadtime_ns * 72 / 100038 uint32_t dtg = (deadtime_ns * 72) / 1000;39 if (dtg > 0x7F) dtg = 0x7F; // 最大127 → ~1.76μs40 TIM1->BDTR = (TIM1->BDTR & ~TIM_BDTR_DTG) | (dtg & TIM_BDTR_DTG);41 TIM1->BDTR |= TIM_BDTR_MOE; // 主输出使能42 TIM_CCxNCmd(TIM1, TIM_Channel_1, TIM_CCxN_Enable); // 互补输出43}44 45// 典型值: 100-500ns 用于MOSFET, 1-2μs 用于BJT46// 太小→直通风险, 太大→输出失真</code></pre>47 48<div class="warn">49<strong>L298N需要死区吗?</strong> L298N的BJT开关速度慢(μs级),自带"天然死区",所以接IN1/IN2反向控制时不需要额外插入死区。但换用MOSFET驱动器时必须配置!50</div>51 52<h2>📐 电机电流采样与保护</h2>53<pre><code>// 使用ADC采样L298N的电流检测引脚(ISENA/ISENB)54// ISEN输出 = 电机电流 × Rsense × 增益55// 典型Rsense=0.5Ω, V_ISEN = I_motor × 0.5V/A56 57void Motor_CurrentMonitor(void) {58 HAL_ADC_Start(&hadc1);59 if (HAL_ADC_PollForConversion(&hadc1, 10) == HAL_OK) {60 uint32_t adc_val = HAL_ADC_GetValue(&hadc1);61 float voltage = adc_val * 3.3f / 4096.0f;62 float current = voltage / 0.5f; // I = V / Rsense63 64 // 过流保护 - 超过2A立即停止65 if (current > 2.0f) {66 TIM3->CCR1 = 0; // PWM=067 HAL_GPIO_WritePin(IN1_GPIO_Port, IN1_Pin, GPIO_PIN_RESET);68 HAL_GPIO_WritePin(IN2_GPIO_Port, IN2_Pin, GPIO_PIN_RESET);69 }70 }71}72 73// 也可使用STM32内部比较器(COMP)实现硬件级过流保护74// COMP1_INP → ISEN信号, COMP1_INM → DAC设定阈值75// 触发BRK刹车功能 → 硬件自动关闭PWM输出 → 延迟<100ns</code></pre>76 77<h2>🔄 无刷直流电机(BLDC)入门</h2>78<p>BLDC电机效率更高(85-95%)、寿命更长,是无人机/电动车的主流选择。控制方式分为:</p>79<table>80<tr><th>控制方式</th><th>复杂度</th><th>效率</th><th>噪音</th><th>应用</th></tr>81<tr><td>六步换相(6-step)</td><td>低</td><td>中</td><td>大(转矩波动)</td><td>风扇/水泵</td></tr>82<tr><td>正弦波控制</td><td>中</td><td>高</td><td>小</td><td>家用电器</td></tr>83<tr><td>FOC (矢量控制)</td><td>高</td><td>最高</td><td>最低</td><td>机器人/电动车</td></tr>84</table>85 86<div class="tip">87<strong>从有刷到无刷的学习路径:</strong> 有刷PWM → 理解换相原理 → 六步换相(Hall传感器) → 反电动势检测(无传感器) → FOC(Clark/Park变换+电流环+速度环) → MTPA(最大转矩电流比)88</div>89 90<h2>🔬 用逻辑分析仪调试PWM信号</h2>91<pre><code>调试步骤:921. 逻辑分析仪CH0接PWM输出引脚(如PA6=TIM3_CH1)932. 设置采样率≥10MHz (1kHz PWM需要至少100x采样率)943. 触发模式: 上升沿触发954. 测量: 周期→验证频率, 高电平宽度/周期→验证占空比96 97常见异常:98- 占空比不随CCR变化 → 检查CCR是否更新到影子寄存器(TIM预装载)99- 频率不是期望值 → 检查PSC/ARR计算, 确认TIM时钟源(APB1=36MHz或APB2=72MHz?)100- 输出一直为高/低 → 检查GPIO复用功能配置, AFIO重映射</code></pre>101 102<h2>📊 电机控制性能指标</h2>103<table>104<tr><th>指标</th><th>定义</th><th>典型值(L298N)</th><th>优秀值(FOC)</th></tr>105<tr><td>转速精度</td><td>实际/目标转速误差</td><td>±5% (开环)</td><td>±0.1% (闭环)</td></tr>106<tr><td>调速比</td><td>最高/最低稳定转速</td><td>10:1</td><td>1000:1</td></tr>107<tr><td>转矩响应</td><td>负载突变→恢复时间</td><td>500ms</td><td>10ms</td></tr>108<tr><td>低速爬行</td><td>最低稳定转速</td><td>~100RPM</td><td>~1RPM</td></tr>109</table>""",110 111 "proj-servo-control.html": """112<h2>📐 舵机内部结构与PID控制</h2>113<p>标准舵机内部是一个完整的闭环控制系统:</p>114<pre><code>舵机内部框图:115┌─────────┐ ┌──────────┐ ┌──────────┐ ┌─────────┐116│PWM信号 │→ │比较器/ │→ │H桥驱动 │→ │直流电机 │→ 齿轮减速→输出轴117│(500- │ │误差放大 │ │ │ │ │118│ 2500μs)│ └────┬─────┘ └──────────┘ └─────────┘119└─────────┘ │ │120 │ ┌──────────┐ │121 └─────────│电位器反馈│←──────────────┘122 └──────────┘123 124误差 = 目标位置 - 实际位置(电位器电压)125误差 → P放大 → 驱动电机 → 减小误差 → 0误差=停止</code></pre>126 127<h3>舵机死区(Deadband)与精度</h3>128<table>129<tr><th>舵机类型</th><th>死区</th><th>最小角度分辨率</th><th>典型价格</th></tr>130<tr><td>廉价SG90</td><td>~10μs</td><td>~1°</td><td>¥5</td></tr>131<tr><td>标准MG995</td><td>~5μs</td><td>~0.5°</td><td>¥15</td></tr>132<tr><td>数字舵机(DS3225)</td><td>~2μs</td><td>~0.2°</td><td>¥40</td></tr>133<tr><td>磁编码舵机</td><td>~0.5μs</td><td>~0.05°</td><td>¥200+</td></tr>134</table>135 136<h2>🔄 模拟舵机 vs 数字舵机</h2>137<table>138<tr><th>特性</th><th>模拟舵机</th><th>数字舵机</th></tr>139<tr><td>控制频率</td><td>50Hz (20ms帧)</td><td>最高333Hz (3ms帧)</td></tr>140<tr><td>死区</td><td>5-10μs</td><td>0.5-2μs</td></tr>141<tr><td>响应速度</td><td>0.2-0.3s/60°</td><td>0.05-0.1s/60°</td></tr>142<tr><td>静止耗电</td><td>低(仅电位器)</td><td>高(MCU持续工作)</td></tr>143<tr><td>锁定力矩</td><td>中</td><td>高(高频修正)</td></tr>144<tr><td>典型应用</td><td>航模/教学</td><td>机器人/无人机</td></tr>145</table>146 147<div class="warn">148<strong>数字舵机注意事项:</strong> 数字舵机需要更高的控制频率(100-333Hz),如果仍用50Hz信号,它会在每帧之间"睡着"再"醒来",表现为抖动和发热。用TIM的PWM频率参数调整:72MHz/(PSC+1)/(ARR+1) ≈ 250Hz。例如PSC=71, ARR=3999 → 250Hz。149</div>150 151<h2>🧮 多舵机同步控制算法</h2>152<pre><code>// 多舵机插值同步 - 使多个舵机同时到达目标位置153// 避免"各走各的"的机械抖动154 155#define SERVO_COUNT 6156 157typedef struct {158 uint16_t start_pulse; // 起始脉宽 (500-2500)159 uint16_t target_pulse; // 目标脉宽160 uint16_t current_pulse; // 当前脉宽161 uint32_t start_time; // 起始时间戳(ms)162 uint32_t duration; // 运动时长(ms)163} ServoChannel;164 165ServoChannel servos[SERVO_COUNT];166 167void Servo_MoveSync(uint16_t targets[], uint32_t duration_ms) {168 // 找出需要最长时间移动的舵机169 uint32_t max_travel = 0;170 for (int i = 0; i < SERVO_COUNT; i++) {171 uint32_t travel = abs((int)targets[i] - (int)servos[i].current_pulse);172 if (travel > max_travel) max_travel = travel;173 }174 175 // 按最大行程分配时间,所有舵机同步完成176 for (int i = 0; i < SERVO_COUNT; i++) {177 servos[i].start_pulse = servos[i].current_pulse;178 servos[i].target_pulse = targets[i];179 servos[i].start_time = HAL_GetTick();180 uint32_t travel = abs((int)targets[i] - (int)servos[i].current_pulse);181 servos[i].duration = (max_travel > 0) 182 ? (travel * duration_ms) / max_travel 183 : duration_ms;184 }185}186 187void Servo_Update(void) {188 // 在SysTick中断或RTOS任务中调用189 uint32_t now = HAL_GetTick();190 for (int i = 0; i < SERVO_COUNT; i++) {191 if (servos[i].current_pulse == servos[i].target_pulse) continue;192 uint32_t elapsed = now - servos[i].start_time;193 if (elapsed >= servos[i].duration) {194 servos[i].current_pulse = servos[i].target_pulse;195 } else {196 // 线性插值197 float t = (float)elapsed / servos[i].duration;198 // 使用缓动函数让运动更平滑199 t = t * t * (3.0f - 2.0f * t); // smoothstep200 servos[i].current_pulse = servos[i].start_pulse 201 + (uint16_t)((servos[i].target_pulse - servos[i].start_pulse) * t);202 }203 TIMx->CCRx = servos[i].current_pulse; // 更新对应通道204 }205}</code></pre>206 207<h2>🎯 总线舵机协议(UART/TTL)</h2>208<p>高端机器人舵机(如Dynamixel、SCS系列)使用串行总线协议替代PWM控制,支持位置/速度/温度/电压反馈:</p>209<pre><code>// Dynamixel Protocol 2.0 示例210// 指令格式: [0xFF,0xFF,0xFD,0x00, ID, Len_L,Len_H, INST, PARAM..., CRC_L,CRC_H]211// INST=0x03: 写位置 (Goal Position)212 213uint8_t dxl_write_position(uint8_t id, uint16_t position) {214 uint8_t packet[] = {215 0xFF, 0xFF, 0xFD, 0x00, // Header216 id, // ID217 0x07, 0x00, // Length (7 bytes)218 0x03, // Instruction: WRITE219 0x74, 0x00, // Address: Goal Position (116)220 position & 0xFF, position >> 8, // Position (0-4095)221 position & 0xFF, position >> 8, // Position (0-4095)222 0x00, 0x00 // CRC (calculate)223 };224 // 计算CRC16...225 return uart_send(packet, sizeof(packet));226}227 228// 电压反馈(Address 144): 实际电压 = 读取值 × 0.1V229// 温度反馈(Address 146): 实际温度 = 读取值 (°C)230// 负载反馈(Address 126): 0-1023 → 0-100% 最大扭矩</code></pre>231 232<h2>⚙️ 舵机电源设计要点</h2>233<div class="warn">234<strong>多舵机系统的电源陷阱:</strong>235<ul>236<li>单个SG90堵转电流可达750mA,6个=4.5A!</li>237<li>STM32开发板的5V输出通常仅500mA,带上2个舵机就可能复位</li>238<li>解法:舵机独立供电(5-6V/3-5A开关电源),共地接STM32</li>239<li>舵机启动瞬间电流可达额定值3-5倍,电源需留裕量</li>240<li>信号线串100Ω电阻防止舵机噪声耦合回MCU</li>241</ul>242</div>""",243 244 "proj-ultrasonic.html": """245<h2>📡 超声波传感器阵列与多目标检测</h2>246<pre><code>// 多个HC-SR04同时工作会互相干扰247// 方案1: 分时触发 - 轮流触发,每个等60ms(最大测量距离4m)248// 方案2: 不同频率 - 定制不同频率的探头(40kHz/25kHz/58kHz)249 250// 冲突避免算法 - 基于随机退避251#define SONAR_COUNT 4252 253typedef struct {254 GPIO_TypeDef* trig_port;255 uint16_t trig_pin;256 GPIO_TypeDef* echo_port;257 uint16_t echo_pin;258 uint32_t last_trigger;259 float distance_cm;260} SonarSensor;261 262void Sonar_TriggerWithAvoidance(SonarSensor* sn, uint8_t index) {263 // 检查是否有其他传感器正在测量264 uint32_t now = HAL_GetTick();265 for (int i = 0; i < SONAR_COUNT; i++) {266 if (i == index) continue;267 uint32_t since = now - sonars[i].last_trigger;268 if (since < 50) { // 50ms内有人在工作269 return; // 等下一轮270 }271 }272 // 安全触发273 HAL_GPIO_WritePin(sn->trig_port, sn->trig_pin, GPIO_PIN_SET);274 delay_us(10);275 HAL_GPIO_WritePin(sn->trig_port, sn->trig_pin, GPIO_PIN_RESET);276 sn->last_trigger = now;277}</code></pre>278 279<h2>🔬 超声波信号处理与滤波</h2>280<table>281<tr><th>方法</th><th>效果</th><th>实现复杂度</th><th>适用场景</th></tr>282<tr><td>中值滤波</td><td>去除偶然野点</td><td>低(3-5样本排序取中)</td><td>基本抗干扰</td></tr>283<tr><td>滑动平均</td><td>平滑数据</td><td>低(队列+累加/除)</td><td>实时显示</td></tr>284<tr><td>卡尔曼滤波</td><td>最优估计+预测</td><td>中(6个浮点运算)</td><td>机器人定位</td></tr>285<tr><td>互相关匹配</td><td>识别特定波形</td><td>高(DSP/FFT)</td><td>高级回波分析</td></tr>286</table>287 288<pre><code>// 超声波距离测量的卡尔曼滤波器289typedef struct {290 float x; // 状态(距离估计)291 float P; // 估计误差协方差292 float Q; // 过程噪声 (越小越信任模型, 典型0.01)293 float R; // 测量噪声 (越小越信任传感器, 典型5.0)294} KalmanFilter1D;295 296float Kalman_Update(KalmanFilter1D* kf, float measurement) {297 // 预测298 kf->P = kf->P + kf->Q;299 // 更新300 float K = kf->P / (kf->P + kf->R); // 卡尔曼增益301 kf->x = kf->x + K * (measurement - kf->x);302 kf->P = (1.0f - K) * kf->P;303 return kf->x;304}305 306// 使用示例:307KalmanFilter1D sonar_kf = {.x=100, .P=1, .Q=0.01, .R=5.0};308float raw = Sonar_ReadDistance();309float filtered = Kalman_Update(&sonar_kf, raw);310// filtered响应平滑,不会因单个异常值跳变</code></pre>311 312<h2>🌡️ 温度补偿与声速修正</h2>313<p>声速随温度变化:v = 331.3 + 0.606 × T (m/s),T为摄氏温度。在-10°C到40°C范围内,声速变化约15%,距离测量误差同比例。</p>314<pre><code>// 带温度补偿的距离计算315float Sonar_DistanceWithTemp(float pulse_us, float temp_c) {316 float speed_of_sound = 331.3f + 0.606f * temp_c; // m/s317 float speed_cm_us = speed_of_sound / 10000.0f; // cm/μs (除以100×100)318 return (pulse_us * speed_cm_us) / 2.0f; // 除以2(往返)319}320 321// 不同温度下的声速对照:322// -10°C: 325.2 m/s → 1cm需要61.5μs往返323// 20°C: 343.4 m/s → 1cm需要58.2μs往返324// 40°C: 355.5 m/s → 1cm需要56.2μs往返</code></pre>325 326<h2>📐 波束角与安装策略</h2>327<table>328<tr><th>型号</th><th>波束角</th><th>盲区</th><th>最大距离</th><th>精度</th></tr>329<tr><td>HC-SR04</td><td>~15°</td><td>2cm</td><td>4m</td><td>±3mm</td></tr>330<tr><td>JSN-SR04T(防水)</td><td>~75°</td><td>25cm</td><td>4.5m</td><td>±1cm</td></tr>331<tr><td>US-100</td><td>~15°</td><td>2cm</td><td>4.5m</td><td>±1mm</td></tr>332<tr><td>AJ-SR04M(防水)</td><td>~75°</td><td>20cm</td><td>8m</td><td>±1cm</td></tr>333</table>334 335<div class="note">336<strong>波束角实战意义:</strong> 15°波束角下,4m距离的覆盖直径约1.05m。如果检测窄小物体(如桌腿直径5cm),需要精确对准。75°广角防水探头适合液位测量,但盲区大(25cm)。337</div>338 339<h2>🎯 超声波 vs 其他测距方案对比</h2>340<table>341<tr><th>方案</th><th>精度</th><th>距离</th><th>刷新率</th><th>受光线影响</th><th>价格</th></tr>342<tr><td>HC-SR04(超声)</td><td>±3mm</td><td>2cm-4m</td><td>25Hz</td><td>不受影响</td><td>¥3</td></tr>343<tr><td>VL53L0X(ToF激光)</td><td>±1mm</td><td>3cm-2m</td><td>50Hz</td><td>不受影响(940nm)</td><td>¥15</td></tr>344<tr><td>GP2Y0A21(红外)</td><td>±3cm</td><td>10-80cm</td><td>40Hz</td><td>严重影响</td><td>¥8</td></tr>345<tr><td>TFMini Plus(激光雷达)</td><td>±1cm</td><td>10cm-12m</td><td>1000Hz</td><td>室内不受影响</td><td>¥120</td></tr>346</table>""",347 348 "proj-usb-hid.html": """349<h2>📋 HID报告描述符深度解析</h2>350<p>HID报告描述符是USB设备与主机之间最关键的"契约",定义了数据包格式。错误的描述符会导致设备无法识别。</p>351 352<pre><code>// 自定义复合HID设备描述符(键盘+多媒体键+鼠标)353// 使用HID Descriptor Tool (USB-IF官方工具)可视化编辑354 355__ALIGN_BEGIN static const uint8_t HID_Composite_ReportDesc[] __ALIGN_END = {356 // === 键盘部分 (Report ID 1) ===357 0x05, 0x01, // USAGE_PAGE (Generic Desktop)358 0x09, 0x06, // USAGE (Keyboard)359 0xa1, 0x01, // COLLECTION (Application)360 0x85, 0x01, // REPORT_ID (1) ← 键盘361 362 // 修饰键位图(8bit): Ctrl/Shift/Alt/GUI363 0x05, 0x07, // USAGE_PAGE (Keyboard/Keypad)364 0x19, 0xe0, // USAGE_MINIMUM (Left Control)365 0x29, 0xe7, // USAGE_MAXIMUM (Right GUI)366 0x15, 0x00, // LOGICAL_MINIMUM (0)367 0x25, 0x01, // LOGICAL_MAXIMUM (1)368 0x75, 0x01, // REPORT_SIZE (1)369 0x95, 0x08, // REPORT_COUNT (8)370 0x81, 0x02, // INPUT (Data,Var,Abs)371 372 // 保留字节373 0x95, 0x01, // REPORT_COUNT (1)374 0x75, 0x08, // REPORT_SIZE (8)375 0x81, 0x01, // INPUT (Cnst,Ary,Abs)376 377 // 按键数组(6个同时按下)378 0x95, 0x06, // REPORT_COUNT (6)379 0x75, 0x08, // REPORT_SIZE (8)380 0x15, 0x00, // LOGICAL_MINIMUM (0)381 0x25, 0x65, // LOGICAL_MAXIMUM (101)382 0x05, 0x07, // USAGE_PAGE (Keyboard)383 0x19, 0x00, // USAGE_MINIMUM (Reserved)384 0x29, 0x65, // USAGE_MAXIMUM (Keyboard Application)385 0x81, 0x00, // INPUT (Data,Ary,Abs)386 387 // LED输出(大小写锁定等)388 0x95, 0x05, // REPORT_COUNT (5)389 0x75, 0x01, // REPORT_SIZE (1)390 0x05, 0x08, // USAGE_PAGE (LEDs)391 0x19, 0x01, // USAGE_MINIMUM (Num Lock)392 0x29, 0x05, // USAGE_MAXIMUM (Kana)393 0x91, 0x02, // OUTPUT (Data,Var,Abs)394 0x95, 0x01, // REPORT_COUNT (1)395 0x75, 0x03, // REPORT_SIZE (3) // padding396 0x91, 0x01, // OUTPUT (Cnst,Ary,Abs)397 0xc0, // END_COLLECTION398 399 // === 鼠标部分 (Report ID 2) ===400 0x05, 0x01, // USAGE_PAGE (Generic Desktop)401 0x09, 0x02, // USAGE (Mouse)402 0xa1, 0x01, // COLLECTION (Application)403 0x85, 0x02, // REPORT_ID (2) ← 鼠标404 0x09, 0x01, // USAGE (Pointer)405 0xa1, 0x00, // COLLECTION (Physical)406 407 // 按键(3个)408 0x05, 0x09, // USAGE_PAGE (Button)409 0x19, 0x01, // USAGE_MINIMUM (Button 1)410 0x29, 0x03, // USAGE_MAXIMUM (Button 3)411 0x15, 0x00, // LOGICAL_MINIMUM (0)412 0x25, 0x01, // LOGICAL_MAXIMUM (1)413 0x95, 0x03, // REPORT_COUNT (3)414 0x75, 0x01, // REPORT_SIZE (1)415 0x81, 0x02, // INPUT (Data,Var,Abs)416 417 // 填充418 0x95, 0x01, // REPORT_COUNT (1)419 0x75, 0x05, // REPORT_SIZE (5)420 0x81, 0x01, // INPUT (Cnst)421 422 // X, Y, 滚轮 (相对值)423 0x05, 0x01, // USAGE_PAGE (Generic Desktop)424 0x09, 0x30, // USAGE (X)425 0x09, 0x31, // USAGE (Y)426 0x09, 0x38, // USAGE (Wheel)427 0x15, 0x81, // LOGICAL_MINIMUM (-127)428 0x25, 0x7f, // LOGICAL_MAXIMUM (127)429 0x75, 0x08, // REPORT_SIZE (8)430 0x95, 0x03, // REPORT_COUNT (3)431 0x81, 0x06, // INPUT (Data,Var,Rel)432 0xc0, // END_COLLECTION433 0xc0, // END_COLLECTION434};435 436// 发送键盘报告437typedef struct __packed {438 uint8_t report_id; // = 1439 uint8_t modifiers;440 uint8_t reserved;441 uint8_t keys[6];442} KeyboardReport;443 444// 发送鼠标报告445typedef struct __packed {446 uint8_t report_id; // = 2447 uint8_t buttons;448 int8_t x;449 int8_t y;450 int8_t wheel;451} MouseReport;</code></pre>452 453<h2>🔄 USB复合设备配置</h2>454<pre><code>// 一个USB设备提供多个HID功能 - 配置描述符结构455// 设备描述符 → 配置描述符 → 接口0(HID键盘) → 接口1(HID鼠标) → 接口2(HID游戏手柄)456 457// 关键:每个HID接口需要独立的端点(或共享端点用Report ID区分)458// 端点资源有限(STM32F103: 8个端点), 需要合理分配:459 460// 端点配置策略:461// EP1 IN: 键盘报告 → 主机 (中断传输, 10ms轮询)462// EP2 IN: 鼠标报告 → 主机 (中断传输, 1ms轮询, 更高优先级)463// EP3 OUT: 键盘LED → 设备 (中断传输)464 465// 描述符总大小限制: FS设备配置描述符+接口+端点+报告 ≤ 64字节466// 如果超限 → 使用字符串描述符或报告描述符单独获取</code></pre>467 468<div class="warn">469<strong>USB HID常见坑:</strong>470<ol>471<li>报告描述符中REPORT_COUNT和REPORT_SIZE必须与实际发送数据长度匹配(差1bit都不行)</li>472<li>Windows对HID键盘有特殊处理:即使你不发送按键,也必须定期发送空报告(否则系统认为设备断开)</li>473<li>复合设备的接口描述符中bInterfaceClass=0x03 (HID),但IAD描述符的bFunctionClass可以是0x00或0x03</li>474<li>Linux的usbhid驱动对report descriptor语法更严格,一些Windows接受的错误在Linux上报错</li>475</ol>476</div>477 478<h2>🎮 HID游戏手柄实现</h2>479<pre><code>// 标准游戏手柄报告描述符要点:480// Usage Page: 0x01 (Generic Desktop)481// Usage: 0x05 (Game Pad)482// 常见轴: X, Y, Z, Rx, Ry, Rz, Hat Switch, Slider483// 按钮: Usage Page 0x09 (Button), 最多32个484 485// 最小化游戏手柄报告 (8轴 + 16按键 = 12字节)486typedef struct __packed {487 uint8_t report_id; // 默认0488 // 轴 (每个2字节, 范围-32768~32767, 但游戏通常用0~65535)489 int16_t x, y; // 左摇杆490 int16_t z, rz; // 右摇杆 (Z=左扳机, Rz=右扳机)491 int16_t rx, ry; // 可选轴492 uint8_t hat; // 方向键 (0-7 = N→NW, 8=居中)493 // 按钮位图494 uint16_t buttons; // 16个按钮495} GamepadReport;496 497// 注意: 使用HID Gamepad需要报告描述符声明Physical Descriptor498// Set Physical Descriptor Set (0x330-0x33F) 定义轴的物理含义499// 游戏通常不严格校验, 但XInput兼容需要特定映射</code></pre>""",500 501 "proj-ir-remote.html": """502<h2>📡 红外遥控协议深度对比</h2>503<table>504<tr><th>协议</th><th>载波频率</th><th>编码方式</th><th>地址位</th><th>命令位</th><th>数据长度</th><th>典型设备</th></tr>505<tr><td>NEC</td><td>38kHz</td><td>脉冲距离</td><td>8+8(反)</td><td>8+8(反)</td><td>32bit</td><td>日系家电</td></tr>506<tr><td>Sony SIRC</td><td>40kHz</td><td>脉冲宽度</td><td>5/8/13</td><td>7</td><td>12/15/20bit</td><td>Sony全系列</td></tr>507<tr><td>RC-5</td><td>36kHz</td><td>Manchester</td><td>5</td><td>6</td><td>14bit</td><td>飞利浦</td></tr>508<tr><td>RC-6</td><td>36kHz</td><td>Manchester+</td><td>8</td><td>8</td><td>20bit+</td><td>飞利浦新款</td></tr>509<tr><td>Samsung</td><td>38kHz</td><td>脉冲距离</td><td>16</td><td>16+16</td><td>48bit</td><td>三星电视</td></tr>510</table>511 512<h3>NEC协议细节(最常用)</h3>513<pre><code>NEC帧结构:514┌──────┐┌───┐ ┌─┐┌─┐┌─┐ ┌─┐┌─┐┌─┐┌─┐515│9ms ││4.5│ │ ││ ││ │ ... │ ││ ││ ││ │516│引导码││ms │ │0││1││1│ │0││0││1││0│517└──────┘└───┘ └─┘└─┘└─┘ └─┘└─┘└─┘└─┘518 Leader 地址(8bit) 地址反(8bit) 命令(8bit) 命令反(8bit)519 总32bit数据 + 1bit Stop520 521逻辑0: 562.5μs载波 + 562.5μs空闲 = 1.125ms522逻辑1: 562.5μs载波 + 1687.5μs空闲 = 2.25ms523重复码: 9ms载波 + 2.25ms空闲 + 562.5μs载波(按住不放时每110ms发送)524 525// NEC解码的容差范围(避免因晶振误差导致解码失败)526#define NEC_PULSE 560 // μs527#define NEC_TOLERANCE 0.30 // 30%容差528#define NEC_LEADER_H 9000529#define NEC_LEADER_L 4500530#define NEC_REPEAT_L 2250</code></pre>531 532<h2>🔧 红外学习/克隆功能实现</h2>533<pre><code>// 使用输入捕获测量脉冲宽度 → 存储 → 回放534#define IR_CODE_MAX_LEN 200 // 最大脉冲数(含引导码+停止)535 536typedef struct {537 uint16_t pulses[IR_CODE_MAX_LEN]; // 脉宽序列(μs)538 uint16_t len; // 脉冲数539 uint8_t protocol; // NEC=0, SIRC=1, RAW=2540 uint32_t decoded; // 解码后的值541} IR_Code;542 543// 学习模式:记录所有脉冲宽度,不做解码544void IR_LearnMode(void) {545 // 1. 使能TIM输入捕获(上升沿/下降沿交替)546 // 2. 每次捕获记录脉冲宽度547 // 3. 超时3秒无新脉冲 → 保存548 549 IR_Code code;550 code.len = 0;551 // ... 捕获逻辑 ...552}553 554// 回放模式:按记录脉冲序列输出555void IR_Replay(IR_Code* code) {556 for (int i = 0; i < code->len; i++) {557 IR_SendCarrier(code->pulses[i]); // 38kHz载波开558 if (++i < code->len)559 IR_NoCarrier(code->pulses[i]); // 载波关560 }561}562 563// 压缩存储: 一个32bit NEC码占8字节(原始RAW约200字节)564// 适合做"万能遥控器"的学习功能</code></pre>565 566<h2>📊 红外发射功率与距离</h2>567<table>568<tr><th>电路</th><th>峰值电流</th><th>有效距离</th><th>波束角</th></tr>569<tr><td>GPIO直驱(20mA)</td><td>~20mA</td><td>1-3m</td><td>~30°</td></tr>570<tr><td>三极管驱动(S8050)</td><td>~100mA</td><td>5-8m</td><td>~30°</td></tr>571<tr><td>MOSFET驱动+多管并联</td><td>~500mA</td><td>15-20m</td><td>~30°</td></tr>572<tr><td>TSAL6200专用管@100mA</td><td>100mA</td><td>10-15m</td><td>±17°</td></tr>573</table>574 575<div class="tip">576<strong>提升红外距离的实用技巧:</strong><br>5771. 用PWM+三极管驱动(不要GPIO直驱) → 距离翻3倍<br>5782. 占空比用33%(1/3载波)而非50% → 峰值电流更大但RMS不过热<br>5793. 接收端加红外滤光片 → 减少环境光干扰 → 有效距离提升<br>5804. 38kHz载波用TIM硬件生成 → 比bit-bang准确、不占CPU581</div>582 583<h2>🔐 红外通信安全与加密</h2>584<div class="note">585红外遥控信号是明文的,任何人都可以用接收头捕获。对于门禁、保险箱等安全应用:<br>586- 滚动码(Rolling Code):每次按发送递增计数器+加密,防止重放攻击<br>587- 双向认证:设备收到命令后发送随机挑战码,遥控器计算响应<br>588- 时间戳+签名:防止信号被录制后重放589</div>""",590 591 "proj-stepper-motor.html": """592<h2>📐 步进电机微步细分原理</h2>593<p>全步(full-step)时每一步1.8°(200步/转),通过控制两相电流比例可以实现更小的步进角:</p>594 595<pre><code>// 两相电流与微步位置的关系596// I_A = I_max × cos(θ), I_B = I_max × sin(θ)597// θ = (step / microsteps_per_step) × 90°598 599// 16微步的电流表(正弦表,DAC输出或PWM+滤波)600const uint16_t microstep_sine[16] = {601 // 第一象限 0°-90°602 0, // sin(0°) → 0%603 4006, // sin(5.625°)→ 9.8% (4095×0.098)604 7996, // sin(11.25°)→ 19.5%605 11955, // sin(16.875°)→29.2%606 15866, // sin(22.5°)→ 38.8%607 19714, // sin(28.125°)→48.1%608 23483, // sin(33.75°)→57.3%609 27158, // sin(39.375°)→66.3%610 30725, // sin(45°)→ 75.0%611 34170, // sin(50.625°)→83.4%612 37480, // sin(56.25°)→91.5%613 40643, // sin(61.875°)→99.2%614 43648, // sin(67.5°)→ ~100%(夹紧)615 46484, // sin(73.125°)616 49142, // sin(78.75°)617 51612, // sin(84.375°)618};619 620// 使用TIM高级定时器互补输出 + 死区 + 斩波模式621// A4988/DRV8825内部已实现微步,只需STEP/DIR控制622// 自己用H桥+MCU做微步时才需要上面的正弦表</code></pre>623 624<h2>⚡ S曲线加减速算法</h2>625<pre><code>// S曲线加速 vs 梯形加速626// 梯形: 突然起停 → 机械振动、丢步627// S曲线: 加速度渐变 → 平滑、安静、高速628 629// Bresenham算法 + S曲线速度表630#define ACCEL_STEPS 200 // 加速步数631 632// 生成S曲线频率表(定时器重载值)633uint16_t s_curve_freq[ACCEL_STEPS];634 635void Generate_SCurve(uint32_t start_freq, uint32_t target_freq) {636 for (int i = 0; i < ACCEL_STEPS; i++) {637 float t = (float)i / ACCEL_STEPS; // 0→1638 // S曲线公式: 使用sigmoid或三角函数的对称S639 // f(t) = start + (target-start) × (1-cos(π×t))/2640 float s = (1.0f - cosf(3.14159f * t)) / 2.0f;641 float freq = start_freq + (target_freq - start_freq) * s;642 s_curve_freq[i] = (uint16_t)(SystemCoreClock / freq);643 }644}645 646// 步进脉冲定时器中断647void TIMx_IRQHandler(void) {648 // 更新定时器ARR → 改变脉冲频率649 if (step_index < ACCEL_STEPS) {650 TIMx->ARR = s_curve_freq[step_index++]; // 加速651 } else if (step_index < total_steps - ACCEL_STEPS) {652 TIMx->ARR = s_curve_freq[ACCEL_STEPS-1]; // 匀速653 } else {654 TIMx->ARR = s_curve_freq[total_steps - step_index]; // 减速655 }656}</code></pre>657 658<h2>🔧 常见驱动器芯片对比</h2>659<table>660<tr><th>芯片</th><th>最大电流</th><th>微步</th><th>最大电压</th><th>Rds(on)</th><th>价格</th><th>特点</th></tr>661<tr><td>A4988</td><td>2A</td><td>1/16</td><td>35V</td><td>0.32Ω</td><td>¥5</td><td>最流行</td></tr>662<tr><td>DRV8825</td><td>2.5A</td><td>1/32</td><td>45V</td><td>0.2Ω</td><td>¥8</td><td>高细分</td></tr>663<tr><td>TMC2208</td><td>1.4A</td><td>1/256</td><td>36V</td><td>0.28Ω</td><td>¥12</td><td>超静音</td></tr>664<tr><td>TMC2209</td><td>2.8A</td><td>1/256</td><td>29V</td><td>0.17Ω</td><td>¥18</td><td>StallGuard</td></tr>665<tr><td>TMC5160</td><td>3A</td><td>1/256</td><td>60V</td><td>0.06Ω</td><td>¥55</td><td>高端旗舰</td></tr>666</table>667 668<div class="tip">669<strong>TMC2209的StealthChop2 vs SpreadCycle:</strong><br>670StealthChop2:静音模式,适合3D打印机,低速安静但高速扭矩不足<br>671SpreadCycle:高性能模式,有轻微噪音但全速段扭矩保持<br>672可通过UART配置切换模式,无需改动硬件673</div>674 675<h2>📏 共振与失步问题</h2>676<div class="warn">677<strong>步进电机共振区:</strong> 每台步进电机都有特定的共振频率,在此频率下振动幅度急剧增大,可能导致丢步。常见共振区在50-200RPM。<br><br>678<strong>解决方案:</strong><br>6791. 加速时快速穿过共振区(不在此频率段做匀速)<br>6802. 使用微步驱动(16细分以上,振动减小到1/16)<br>6813. 机械阻尼(加装减震垫、柔性联轴器)<br>6824. 使用TMC系列芯片的共振抑制算法<br>6835. 电流闭环控制(通过反电动势检测丢步)684</div>""",685 686 "proj-can-bus.html": """687<h2>🚗 CAN FD (Flexible Data Rate) 简介</h2>688<p>传统CAN 2.0B最大8字节数据,1Mbps速率。CAN FD突破这些限制:</p>689<table>690<tr><th>特性</th><th>CAN 2.0B</th><th>CAN FD</th></tr>691<tr><td>数据段速率</td><td>最大1 Mbps</td><td>最大8 Mbps</td></tr>692<tr><td>数据长度</td><td>0-8 字节</td><td>0-64 字节</td></tr>693<tr><td>CRC长度</td><td>15 bit</td><td>17/21 bit (自适应)</td></tr>694<tr><td>兼容性</td><td>—</td><td>向下兼容CAN 2.0B节点</td></tr>695<tr><td>BRS位</td><td>无</td><td>波特率切换标志位</td></tr>696</table>697 698<pre><code>// STM32G4/H7/FDCAN外设配置CAN FD699FDCAN_TxHeaderTypeDef TxHeader;700TxHeader.Identifier = 0x321;701TxHeader.IdType = FDCAN_STANDARD_ID;702TxHeader.TxFrameType = FDCAN_DATA_FRAME;703TxHeader.DataLength = FDCAN_DLC_BYTES_64; // 64字节!704TxHeader.ErrorStateIndicator = FDCAN_ESI_ACTIVE;705TxHeader.BitRateSwitch = FDCAN_BRS_ON; // 数据段高速706TxHeader.FDFormat = FDCAN_FD_CAN; // CAN FD格式707 708uint8_t tx_data[64] = {0};709// 填充数据...710HAL_FDCAN_AddMessageToTxFifo(&hfdcan1, &TxHeader, tx_data);</code></pre>711 712<h2>📋 CAN总线仲裁机制详解</h2>713<pre><code>// CAN使用CSMA/CD+AMP (载波监听多路访问/冲突检测+仲裁消息优先级)714// 显性电平=0(主动拉低), 隐性电平=1(被动上拉)715 716// 两个节点同时发送时的仲裁过程:717// 节点A: ID=0x320 = 0011 0010 0000718// 节点B: ID=0x310 = 0011 0001 0000719// 总线: 0011 00-- (在第7bit,B发送1但读到0→知道自己优先级低→立即退出发送)720// 节点A继续: 0011 0010 0000 → 仲裁胜出721 722// 这也就是为什么: ID越小优先级越高! (0x000 = 最高优先级)723// 标准帧ID范围: 0x000 - 0x7FF (11bit)724// 扩展帧ID范围: 0x00000000 - 0x1FFFFFFF (29bit)</code></pre>725 726<h2>🚛 SAE J1939协议概要</h2>727<p>J1939是商用车(卡车/客车/工程机械)的事实标准,基于CAN 2.0B扩展帧:</p>728<pre><code>// J1939使用29bit扩展ID, 结构如下:729// [Priority:3][R/DP:1][PF:8][PS:8][SA:8] = 28bit + 保留730// 731// PF(PDU Format): 0-239 = PDU1(点对点), 240-255 = PDU2(广播)732// PS(PDU Specific): PDU1时为DA(目标地址), PDU2时为GE(组扩展)733// SA: 源地址734 735// 常用PGN(参数组编号):736// PGN 61444 (0xF004): 电子发动机控制器 #1 (EEC1) → 转速、扭矩737// PGN 65262 (0xFEEE): 发动机温度738// PGN 65265 (0xFEF1): 巡航控制/车速739// PGN 65270 (0xFEF6): 进气/排气条件740 741// 例: 请求发动机转速(EEC1)742typedef struct {743 uint8_t prio_res; // Priority=6 → 0x18 (6<<5=0xC0, R=0, DP=0 → 0xC0)744 uint8_t pdu_f; // PF=0x00 → 0x00745 uint8_t pdu_s; // PS=DA(目标地址, 0=发动机, 3=刹车)746 uint8_t src_addr; // SA=源地址747} __attribute__((packed)) J1939_ID;748 749// 请求8字节ID + 0字节数据(远程帧)或带数据的数据帧750// 注意: J1939的多帧传输(>8字节)使用TP(传输协议), BAM广播5秒间隔</code></pre>751 752<h2>🛡️ CAN总线故障诊断</h2>753<table>754<tr><th>错误类型</th><th>现象</th><th>原因</th><th>排查方法</th></tr>755<tr><td>ACK错误</td><td>TEC递增, 重发</td><td>总线上无其他节点</td><td>至少2个节点才能通信</td></tr>756<tr><td>位错误</td><td>发送与回读不一致</td><td>总线短路/终端电阻缺失</td><td>检查H/L间Ω应在45-65Ω</td></tr>757<tr><td>填充错误</td><td>6个连续相同位</td><td>时钟精度/干扰</td><td>检查晶振/SJW配置</td></tr>758<tr><td>格式错误</td><td>固定格式位错误</td><td>CRC分隔符/Delimter被破坏</td><td>检查总线长度≤40m@1Mbps</td></tr>759<tr><td>Bus-Off</td><td>TEC>255</td><td>持续错误</td><td>控制器自动脱离总线</td></tr>760</table>761 762<div class="warn">763<strong>CAN总线调试必备工具:</strong> USB-CAN分析仪(¥30-200) + 上位机软件(CANtest/CANoe/CANalyzer)。可以抓取总线上的所有帧,按ID过滤,统计负载率,模拟节点发送。<br>764<strong>没有分析仪时的土办法:</strong> 用另一个STM32做CAN监听节点,接收所有帧并通过串口转发到电脑。765</div>766 767<h2>⚙️ CAN总线物理层要点</h2>768<pre><code>// CAN_H和CAN_L之间的终端电阻769// 总线两端各一个120Ω → 并联等效60Ω770// 万用表测量CAN_H-CAN_L(断电!)应为~60Ω, 771// 如果60Ω→两端都有终端, 120Ω→只有一端, 开路→没有终端772 773// 总线长度限制:774// 1 Mbps → max 40m775// 500 kbps → max 100m776// 250 kbps → max 250m777// 125 kbps → max 500m778// 50 kbps → max 1000m779 780// 采样点配置 (Sample Point = (1+TSEG1)/(1+TSEG1+TSEG2)):781// 典型值: 75%-87.5%782// 高速: TSEG1=11, TSEG2=4, SJW=1 → SP=(1+11)/(1+11+4)=75%@1Mbps783// 长距离: TSEG1=13, TSEG2=2, SJW=1 → SP=(1+13)/(1+13+2)=87.5%</code></pre>""",784 785 "proj-dma-adc.html": """786<h2>🔄 DMA双缓冲 (Double Buffering) 技术</h2>787<p>DMA采集时如果CPU读取和DMA写入同时操作同一buffer会导致数据混乱。双缓冲技术完美解决:</p>788<pre><code>// DMA半传输完成中断 → Buffer0满, 处理Buffer0789// DMA传输完成中断 → Buffer1满, 处理Buffer1790// 乒乓切换, 零数据丢失791 792#define ADC_BUFFER_SIZE 1024 // 每通道采样点793#define ADC_CHANNELS 4794 795uint16_t adc_buffer[2][ADC_BUFFER_SIZE * ADC_CHANNELS];796volatile uint8_t active_buffer = 0; // 0=处理Buf0, 1=处理Buf1797 798// 配置DMA为循环模式 + 半传输中断799HAL_ADC_Start_DMA(&hadc1, 800 (uint32_t*)adc_buffer, 801 ADC_BUFFER_SIZE * ADC_CHANNELS * 2); // 双倍长度802 803void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef* hadc) {804 // Buffer0 满了 → 标记处理805 active_buffer = 0;806 ProcessADCBlock(adc_buffer[0]); // 处理前半段807}808 809void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) {810 // Buffer1 满了 → 标记处理811 active_buffer = 1;812 ProcessADCBlock(adc_buffer[1]); // 处理后半段813}</code></pre>814 815<h2>⏱️ 定时器触发ADC (Timer-Triggered Sampling)</h2>816<pre><code>// 精确控制采样率,避免HAL_ADC_Start()的软件抖动817// 适用场景: FFT分析、振动测量、电力质量监测818 819// 配置TIM3输出触发ADC1820void TIM_TriggerADC_Config(uint32_t sample_rate_hz) {821 // TIM3 → TRGO → ADC1 (需要配置ADC_ExternalTrigConv)822 // 采样率 = TIM_CLK / ((PSC+1) * (ARR+1))823 824 uint32_t tim_clk = HAL_RCC_GetPCLK1Freq() * 2; // APB1×2825 uint32_t prescaler = 0;826 uint32_t period = tim_clk / sample_rate_hz - 1;827 828 if (period > 65535) {829 prescaler = period / 65536; // 需要分频830 period = period / (prescaler + 1) - 1;831 }832 833 htim3.Init.Prescaler = prescaler;834 htim3.Init.Period = period;835 htim3.Init.CounterMode = TIM_COUNTERMODE_UP;836 HAL_TIM_Base_Init(&htim3);837 838 // 配置TRGO为更新事件839 TIM_MasterConfigTypeDef sMasterConfig = {0};840 sMasterConfig.MasterOutputTrigger = TIM_TRGO_UPDATE;841 sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;842 HAL_TIMEx_MasterConfigSynchronization(&htim3, &sMasterConfig);843 844 // ADC配置外部触发845 sConfig.ExternalTrigConv = ADC_EXTERNALTRIGCONV_T3_TRGO;846 // 注意: 不同STM32系列的ADC外部触发源不同!847 // F103: ADC1→TIM1/TIM2/TIM3, F4: ADC1→TIM2/TIM3/TIM4/TIM5/TIM8848}849 850// 实际采样率验证:851// 用另一个TIM做计数器,1秒后读取采样数 = 实际采样率852// 通常实际值会比理论值偏小2-5% (DMA总线仲裁延迟)</code></pre>853 854<h2>📊 DMA+ADC+FFT实时频谱分析</h2>855<pre><code>// ARM CMSIS-DSP FFT流水线856#include "arm_math.h"857 858#define FFT_SIZE 1024859 860float32_t fft_input[FFT_SIZE * 2]; // 实部+虚部交错861float32_t fft_output[FFT_SIZE];862 863void ProcessFFT(uint16_t* raw_buffer) {864 // 1. 转浮点 + 去直流865 float32_t mean = 0;866 for (int i = 0; i < FFT_SIZE; i++)867 mean += raw_buffer[i];868 mean /= FFT_SIZE;869 870 for (int i = 0; i < FFT_SIZE; i++) {871 fft_input[i * 2] = raw_buffer[i] - mean; // 实部872 fft_input[i * 2 + 1] = 0; // 虚部(0)873 }874 875 // 2. FFT (使用CMSIS-DSP的RFFT, 比CFFT快50%)876 arm_rfft_fast_instance_f32 rfft;877 arm_rfft_fast_init_f32(&rfft, FFT_SIZE);878 arm_rfft_fast_f32(&rfft, fft_input, fft_output, 0);879 880 // 3. 计算幅值881 // fft_output[0] = 直流分量, fft_output[1] = 实部(频率1)882 // fft_output[FFT_SIZE/2] = 虚部(频率1)883 // 频率分辨率 = 采样率 / FFT_SIZE884 arm_cmplx_mag_f32(fft_output, fft_input, FFT_SIZE / 2);885 886 // 4. 查找峰值频率887 float32_t max_mag = 0;888 uint32_t max_bin = 0;889 for (int i = 1; i < FFT_SIZE / 2; i++) { // 跳过DC890 if (fft_input[i] > max_mag) {891 max_mag = fft_input[i];892 max_bin = i;893 }894 }895 float32_t peak_freq = (float32_t)max_bin * sample_rate / FFT_SIZE;896}</code></pre>897 898<h2>⚡ DMA传输优化技巧</h2>899<table>900<tr><th>优化项</th><th>效果</th><th>代价</th></tr>901<tr><td>使用DMA FIFO</td><td>减少总线占用(4→1次)</td><td>FIFO满才传输, 延迟增加</td></tr>902<tr><td>DMA突发传输(Burst)</td><td>吞吐量翻倍</td><td>需要外设支持</td></tr>903<tr><td>SRAM分区(D2域)</td><td>避免CPU+DMA抢总线</td><td>F4/H7才有</td></tr>904<tr><td>DMA半传输中断</td><td>实现零拷贝双缓冲</td><td>代码复杂度+</td></tr>905<tr><td>链接列表(BDMA)</td><td>复杂采集模式</td><td>仅G4/H7/U5</td></tr>906</table>907 908<div class="warn">909<strong>DMA常见坑:</strong><br>9101. DMA传输长度 = 外设数据宽度 × 计数,配置错误会导致少传/多传<br>9112. ADC采样时间太短(<7.5周期) + 高频率 → DMA取走的数据是上一次的<br>9123. Cache一致性问题(H7系列):DMA写到SRAM,CPU读Cache → 需要SCB_CleanInvalidateDCache<br>9134. STM32F103的DMA不能跨AHB总线 → 检查数据源和目标是否在同一总线914</div>""",915 916 "proj-low-power.html": """917<h2>💤 STM32低功耗模式深度对比</h2>918<table>919<tr><th>模式</th><th>功耗(F103@72MHz)</th><th>唤醒源</th><th>唤醒时间</th><th>SRAM保持</th><th>适用场景</th></tr>920<tr><td>Run(正常运行)</td><td>~36mA</td><td>—</td><td>—</td><td>全部</td><td>高性能计算</td></tr>921<tr><td>Sleep(睡眠)</td><td>~14mA</td><td>任意中断</td><td>即刻</td><td>全部</td><td>等待事件</td></tr>922<tr><td>Stop(停止)</td><td>~25μA</td><td>EXTI/RTC/WKUP</td><td>~6μs</td><td>全部</td><td>间歇采样</td></tr>923<tr><td>Standby(待机)</td><td>~3μA</td><td>WKUP/RTC/NRST</td><td>~60μs</td><td>丢失</td><td>电池供电传感器</td></tr>924<tr><td>Shutdown(F4+)</td><td>~0.3μA</td><td>WKUP/RTC/NRST</td><td>~200μs</td><td>丢失</td><td>超长待机</td></tr>925</table>926 927<pre><code>// Stop模式的正确用法 (功耗: 36mA → 25μA, 降低1400倍!)928void Enter_StopMode(void) {929 // 1. 关闭不需要的外设时钟930 __HAL_RCC_GPIOA_CLK_DISABLE(); // 只保留唤醒源GPIO931 __HAL_RCC_USART1_CLK_DISABLE();932 __HAL_RCC_SPI1_CLK_DISABLE();933 934 // 2. 配置所有未用GPIO为模拟输入(最低功耗)935 GPIO_InitTypeDef gpio = {0};936 gpio.Mode = GPIO_MODE_ANALOG;937 gpio.Pull = GPIO_NOPULL;938 for (int pin = 0; pin <= 15; pin++) {939 gpio.Pin = 1 << pin;940 HAL_GPIO_Init(GPIOA, &gpio);941 }942 943 // 3. 配置唤醒源944 HAL_PWR_EnableWakeUpPin(PWR_WAKEUP_PIN1); // PA0上升沿唤醒945 946 // 4. 进入Stop模式947 HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON, PWR_STOPENTRY_WFI);948 949 // 5. 唤醒后: 恢复时钟和外设950 SystemClock_Config(); // 重新配置时钟951}</code></pre>952 953<h2>🔋 电池寿命估算</h2>954<pre><code>// 电池容量计算955// 假设: 2000mAh锂电池 + Stop模式(25μA) + 每10秒唤醒50ms采样(14mA)956 957// 平均电流 = (待机电流 × 待机时间 + 工作电流 × 工作时间) / 总时间958// = (25μA × 9950ms + 14mA × 50ms) / 10000ms959// = (0.24875 + 0.7) / 10 = 0.0949 mA960 961// 续航 = 2000mAh / 0.0949mA = 21075小时 ≈ 878天 ≈ 2.4年!962 963// 如果不用低功耗(Run模式永远36mA):964// 续航 = 2000mAh / 36mA = 55小时 ≈ 2.3天965 966// 结论: Stop模式把电池寿命从2天提升到2年!</code></pre>967 968<h2>⏰ RTC闹钟唤醒策略</h2>969<pre><code>// 周期性唤醒 + 执行任务 + 快速回睡970void RTC_Wakeup_Config(uint32_t interval_seconds) {971 // 使用RTC的Wakeup定时器(不需要额外时钟源)972 __HAL_RCC_PWR_CLK_ENABLE();973 HAL_PWR_EnableBkUpAccess();974 975 // LSI = ~32kHz, 配置每interval_seconds秒唤醒一次976 // RTC_WAKEUPCLOCK_RTCCLK_DIV16 = 32kHz/16 = 2kHz977 uint32_t wakeup_counter = interval_seconds * 2000 - 1;978 979 HAL_RTCEx_SetWakeUpTimer_IT(&hrtc, wakeup_counter, 980 RTC_WAKEUPCLOCK_RTCCLK_DIV16);981}982 983void HAL_RTCEx_WakeUpTimerEventCallback(RTC_HandleTypeDef *hrtc) {984 // 1. 快速采样传感器985 uint16_t temp = ReadTemperature(); // ~10ms986 uint16_t humidity = ReadHumidity(); // ~10ms987 988 // 2. 判断是否需要上报989 if (abs(temp - last_temp) > 2.0f) { // 温度变化>2°C才发990 LoRa_Send(temp, humidity); // ~100ms991 last_temp = temp;992 }993 994 // 3. 回到Stop模式 (总占用: 20-120ms)995 Enter_StopMode();996}</code></pre>997 998<h2>📊 实际功耗测量方法</h2>999<div class="note">1000<strong>精确测量低功耗电流的方法:</strong><br>10011. 串联Ω级采样电阻(如10Ω)在电源回路<br>10022. 用高精度万用表测量电阻两端电压 → I = V/R<br>10033. 或在电源回路串入万用表电流档(注意: μA档内阻较大, 唤醒瞬间可能导致MCU复位)<br>10044. 更专业的方法: Nordic PPK2或Joulescope(能捕捉μs级瞬态电流)<br>10055. 注意: 杜邦线接触电阻、面包板寄生电容都可能增加功耗5-10μA1006</div>""",1007 1008 "proj-s32k144-auto.html": """1009<h2>🚗 汽车功能安全标准 ISO 26262</h2>1010<table>1011<tr><th>ASIL等级</th><th>单点故障指标</th><th>潜在故障指标</th><th>安全目标示例</th></tr>1012<tr><td>QM</td><td>无要求</td><td>无要求</td><td>信息娱乐系统</td></tr>1013<tr><td>ASIL A</td><td>≥90%</td><td>≥60%</td><td>座椅加热</td></tr>1014<tr><td>ASIL B</td><td>≥90%</td><td>≥60%</td><td>仪表盘、车灯</td></tr>1015<tr><td>ASIL C</td><td>≥97%</td><td>≥80%</td><td>自适应巡航</td></tr>1016<tr><td>ASIL D</td><td>≥99%</td><td>≥90%</td><td>刹车、转向、气囊</td></tr>1017</table>1018 1019<div class="note">1020<strong>S32K144的ASIL B就绪特性:</strong><br>1021- 双核锁步(Dual-core Lockstep) → 检测处理器故障<br>1022- ECC on Flash/SRAM → 单比特纠错、双比特检测<br>1023- 存储器保护单元(MPU) → 防止任务间非法访问<br>1024- 内部看门狗+外部窗口看门狗 → 双级超时保护<br>1025- 时钟监控(CMU) → 检测晶振停振/偏差<br>1026- 电源监控(PMC) → 检测过压/欠压<br>1027- 自检库(S32K SDK自带) → ADC/DAC/Flash测试1028</div>1029 1030<h2>🔌 AUTOSAR架构入门</h2>1031<pre><code>AUTOSAR分层架构:1032┌──────────────────────────────────────┐1033│ 应用层 (Application) │ ← SWC (Software Component)1034├──────────────────────────────────────┤1035│ RTE (Runtime Environment) │ ← 虚拟功能总线(VFB)1036├──────────────────────────────────────┤1037│ 服务层 │ ECU抽象层 │ 复杂驱动 │1038├──────────────────────────────────────┤1039│ MCAL (Microcontroller Abstraction)│ ← CAN/LIN/SPI/I2C/ADC/DIO驱动1040└──────────────────────────────────────┘1041 硬件 (S32K144)1042 1043// MCAL层关键模块:1044// Can → CAN驱动(CAN_Write/CanIf)1045// Lin → LIN驱动(Lin_SendFrame)1046// Dio → 数字IO(Dio_WriteChannel)1047// Adc → ADC驱动(Adc_StartGroupConversion)1048// Pwm → PWM驱动(Pwm_SetDutyCycle)1049// Gpt → 通用定时器(Gpt_StartTimer)1050// Wdg → 看门狗(Wdg_SetTriggerCondition)1051// Eep → EEPROM模拟(Fee_Write)1052 1053// S32K144的AUTOSAR MCAL由NXP提供(需NDA)1054// 开源替代: Arctic Studio + OpenSAR1055// 入门建议: 先用SDK裸机开发理解MCAL, 再引入AUTOSAR</code></pre>1056 1057<h2>🛡️ S32K144安全机制实战</h2>1058<pre><code>// 1. 看门狗配置 (独立的128kHz LPO时钟)1059void WDOG_Config(void) {1060 WDOG->CNT = 0; // 必须先解锁1061 // 超时时间 = (TOVAL+1) × 256 / 128kHz1062 // TOVAL=999 → (1000×256)/128000 = 2秒1063 WDOG->TOVAL = 999;1064 WDOG->CS = WDOG_CS_CLK(1) // LPO时钟1065 | WDOG_CS_EN(1) // 使能1066 | WDOG_CS_WIN(1) // 窗口模式1067 | WDOG_CS_UPDATE(1); // 允许更新1068 // 刷新: WDOG->CNT = 0x02A4 + 0xB480; (顺序不可逆)1069}1070 1071// 2. ECC错误注入测试1072void ECC_Test(void) {1073 // 使能Flash ECC1074 MSCM->OCMDR[0] |= MSCM_OCMDR_ECCEN(1);1075 1076 // 使能ECC中断1077 MSCM->OCMDR[0] |= MSCM_OCMDR_ECCIE(1);1078 1079 // 在ECC ISR中记录错误地址1080 // 单比特错误: 硬件自动纠正, 记录日志1081 // 双比特错误: 不可纠正, 进入安全状态1082}1083 1084// 3. 时钟监控(FCCU)1085void FCCU_Config(void) {1086 // 配置FCCU: 检测时钟丢失 → 切换到内部IRC → 通知系统1087 // NCF(不可配置故障): 时钟丢失 → 直接触发FCCU1088 // CF(可配置故障): ADC异常/DMA错误 → FCCU_CF_S寄存器1089}</code></pre>1090 1091<h2>📟 LIN总线 (S32K144内置LIN控制器)</h2>1092<pre><code>// LIN帧结构:1093// [Sync Break(13+bit低)] [Sync Byte 0x55] [PID] [Data 1-8B] [Checksum]1094// 1095// PID = ID[0:5] | P[0:1] (P0=ID0⊕ID1⊕ID2⊕ID4, P1=¬(ID1⊕ID3⊕ID4⊕ID5))1096// 1097// 经典调度表:1098// Master发送帧头 → 指定的Slave响应 → 下一个帧头 → ...1099 1100// S32K144 LPUART支持LIN模式1101void LPUART_LIN_Config(void) {1102 // 设置13bit Break检测1103 LPUART1->BAUD |= LPUART_BAUD_M10(1); // 13bit模式1104 LPUART1->STAT |= LPUART_STAT_LBKDIF(1); // 使能Break中断1105 1106 // LIN波特率: 19200bps (S32K144 BUSCLK=48MHz)1107 LPUART1->BAUD = LPUART_BAUD_SBR(156) // 48000000/(16×19200)-11108 | LPUART_BAUD_OSR(15); // 16x过采样1109}</code></pre>1110 1111<h2>🧪 汽车级测试与认证</h2>1112<table>1113<tr><th>测试项</th><th>标准</th><th>条件</th><th>目标</th></tr>1114<tr><td>温度循环</td><td>AEC-Q100 G</td><td>-40°C~+125°C, 1000次</td><td>焊接可靠性</td></tr>1115<tr><td>ESD人体模型</td><td>AEC-Q100-002</td><td>±2kV HBM</td><td>抗静电</td></tr>1116<tr><td>ESD充电器件</td><td>AEC-Q100-011</td><td>±500V CDM</td><td>制造过程静电</td></tr>1117<tr><td>闩锁效应</td><td>AEC-Q100-004</td><td>±100mA触发</td><td>不损坏</td></tr>1118<tr><td>潮湿敏感度</td><td>J-STD-020</td><td>MSL3(168h@30°C/60%)</td><td>焊接前吸潮</td></tr>1119</table>""",1120 1121 "protocols.html": """<h2>🔌 逻辑电平转换实战</h2>1122<table>1123<tr><th>方案</th><th>速度</th><th>双向</th><th>通道数</th><th>价格</th><th>适用</th></tr>1124<tr><td>电阻分压(1.8k+3.3k)</td><td>≤1MHz</td><td>单向</td><td>每路2电阻</td><td>¥0.02</td><td>低速单向</td></tr>1125<tr><td>BSS138 MOSFET</td><td>≤2MHz</td><td>双向</td><td>1/片</td><td>¥0.15</td><td>I2C/双向</td></tr>1126<tr><td>TXS0108E</td><td>≤60Mbps</td><td>双向/自动</td><td>8</td><td>¥3</td><td>通用, 推荐!</td></tr>1127<tr><td>74LVC245/4245</td><td>≤100Mbps</td><td>单向</td><td>8</td><td>¥2</td><td>高速并行</td></tr>1128<tr><td>SN74AVC4T774</td><td>≤360Mbps</td><td>双向可配</td><td>4</td><td>¥8</td><td>SD卡/SPI高速</td></tr>1129</table>1130 1131<pre><code>// 3.3V → 5V 电平转换的"野路子"与正规做法1132// 1133// 野路子1: 很多5V MCU的输入高电平阈值Vih=0.7×Vcc=3.5V1134// 3.3V输出不够? → 错! 查数据手册Vih通常=2.0V左右1135//1136// 野路子2: 用GPIO开漏+上拉电阻1137// 5V设备开漏输出→上拉到3.3V→3.3V MCU接收(安全)1138// 3.3V开漏输出→上拉到5V→5V设备接收(注意!查看引脚是否5V耐受)1139//1140// 正规做法: 使用电平转换芯片TXS0108E1141// 无需方向控制、自动双向、8通道、推挽/开漏自适应</code></pre>1142 1143<h2>📡 信号完整性检查清单</h2>1144<div class="warn">1145<strong>排查通信问题的10步checklist:</strong>1146<ol>1147<li>共地了吗?两个设备必须有公共地线!(最常见错误)</li>1148<li>BAUD率误差在允许范围内吗?(标准UART容忍±2%, 内部HSI温度漂移可达±5% → 必须校准或用HSE晶振)</li>1149<li>逻辑电平匹配吗?3.3V↔5V需要转换</li>1150<li>线序对吗?TX↔RX交叉、MOSI↔MOSI直连、SDA/SDA直连</li>1151<li>上拉电阻?I2C必须外接(4.7kΩ@3.3V), SPI的CS需要上拉防浮空</li>1152<li>终端电阻?RS485/RS422/CAN需要在总线两端加120Ω</li>1153<li>线长?UART≤2m, I2C≤30cm(PCB上), SPI≤10cm, RS485≤1200m</li>1154<li>干扰源?电机/继电器/开关电源与信号线保持5cm以上距离</li>1155<li>共模电压?RS485共模范围-7V~+12V, 超出会永久损坏</li>1156<li>总线冲突?RS485多个发送器同时发送→短路→需要方向控制(DE/RE)</li>1157</ol>1158</div>1159 1160<h2>⚡ 用逻辑分析仪查通信问题</h2>1161<pre><code>// Saleae Logic / PulseView 的逻辑分析仪使用技巧1162// 硬件: 8通道/24MHz采样率的CY7C68013A方案(¥25-40)1163 1164// 关键设置:1165// 采样率: ≥ 通信速率的 10倍 (如115200 bps → ≥1.15MHz)1166// 触发: UART→下降沿, I2C→起始条件, SPI→CS下降沿1167// 协议解码: 选择对应协议、设置波特率/极性和相位1168 1169// 常见问题与波形判断:1170// UART:1171// - 全部为0xFF: 线断了或对方未发送1172// - 偶发错误字节: 波特率偏差(对照波形测量位宽)1173// - 只有0x00: TX/RX接反了(检查交叉)1174//1175// I2C:1176// - SDA始终低: 从设备拉死(地址错误或时钟停)1177// - 有SCK无SDA: 上拉电阻缺失或太大1178// - 前7bit对后8bit不对: 地址左移1位?(有些库已做, 有些未做)1179//1180// SPI:1181// - MISO全0: 从设备未正确响应(检查CS时序)1182// - 数据移位: CPOL/CPHA配置错误1183// - 第一个字节错误: CS建立时间不够(在CS拉低后delay 10μs)</code></pre>1184 1185<h2>🔧 通信协议的选择决策树</h2>1186<pre><code>需要通信的两个设备...1187 1188├── 都在同一块PCB上?1189│ ├── 需要多从设备? → I2C (2线, 最多127个从设备)1190│ ├── 需要高速? → SPI (4线, 可达50Mbps)1191│ └── 最简单、只有2个设备? → UART (2线, 常用115200bps)1192│1193├── 距离 2-10米?1194│ ├── 抗干扰要求高? → RS485 (差分, 1200m)1195│ └── 简单点对点? → UART + MAX232 (RS232, 15m)1196│1197├── 距离 10-50米?1198│ └── RS4851199│1200├── 工业/汽车场景?