本文还有配套的精品资源点击获取简介专为STM32F10x系列设计的三相电机开环驱动固件包基于TIM1高级定时器生成六路带可调死区的SPWM波形支持IGBT半桥驱动逻辑内置ADC模块实时采集母线电压与电流信号配合EEPROM24C02和片内Flash实现运行参数如载波频率、调制比、死区时间掉电保存提供ST7565液晶显示驱动、LED状态指示、串口调试通信含serial.h/c、蜂鸣器与按键交互功能所有外设驱动GPIO、TIMER、ADC、I2C、SPI已完成底层封装main.c中仅需修改宏定义即可快速适配不同硬件参数编译输出标准.hex文件可直接通过J-Link等工具烧录适用于小功率三相异步电机或永磁同步电机的SPWM开环控制场景目录结构清晰包含BSP层、uCOS-II移植支持、启动文件、驱动源码及文档资源。1. 项目概述这不是一个“能跑就行”的电机驱动Demo而是一套可直接嵌入小功率三相驱动板的工程级固件你手上拿到的这套代码不是实验室里跑通几个波形就收工的验证程序也不是网上随手搜来的、连死区时间都硬编码在寄存器里的教学例程。它是我过去三年在多个小功率三相风机、水泵、伺服调试平台中反复打磨、量产验证过的SPWM驱动内核——从第一版用标准外设库手撕TIM1寄存器到如今封装成PWM_Init()、ADC_StartConversion()这样一眼就能看懂意图的函数接口中间踩过的坑、调过的波形、烧坏的IGBT模块全沉淀在这份代码里了。核心关键词就五个STM32F10x、SPWM驱动、六路PWM、死区控制、母线采样。但它们组合在一起解决的是一个非常具体、也非常棘手的工程问题如何让一颗主频72MHz的Cortex-M3芯片在不加外部逻辑芯片的前提下安全、稳定、可配置地驱动三相逆变桥答案是——把TIM1高级定时器的所有潜力榨干并用软件逻辑补足硬件的短板。比如“六路互补PWM”很多人以为就是TIM1_CH1/CH1N、CH2/CH2N、CH3/CH3N六个通道各输出一路再手动配对。错。真正可靠的方案是启用TIM1的互补通道自动死区插入刹车功能Break Input让硬件在发生过流或短路时能在纳秒级时间内强制关闭所有高侧MOSFET而不是等CPU响应中断再执行TIM_CCxCmd(TIM1, TIM_Channel_1, DISABLE)——后者慢了至少几十微秒足够让IGBT炸掉。而“母线采样”也不只是接个分压电阻读ADC那么简单母线电压波动剧烈电流采样存在di/dt干扰必须配合数字滤波硬件RC低通采样时序同步比如在PWM低电平中点触发ADC否则你看到的电流值永远在跳PID根本没法调。这套固件面向的不是理论研究者而是拿着原理图、焊好PCB、明天就要带载测试的硬件工程师不是追求矢量控制精度的算法专家而是需要快速验证电机能否转起来、转向是否正确、温升是否正常的现场调试人员。所以它没有复杂的FOC算法但有清晰的LED状态机红灯常亮过压绿灯快闪正在启动黄灯慢闪参数保存成功没有花哨的GUI上位机但串口输入ATVDC?就能返回当前母线电压值输入ATSAVE就能立刻把当前调制比写进24C02——这些细节才是真实项目里最省时间的地方。它不承诺替代工业级驱动器但它能让你在三天内把一块基于STM32F103RCT6的三相驱动板从贴片完成推进到带载运行。下面我们就一层层拆开这个“黑盒子”看看每一行关键代码背后到底在解决什么问题、为什么这么写、以及如果你照着抄最容易在哪一步把板子烧成焦炭。2. 整体架构与设计思路为什么非得用TIM1为什么死区不能靠软件延时2.1 顶层框架裸机驱动 模块化BSP层拒绝RTOS绑架整个固件采用纯裸机轮询中断混合架构未强制依赖uCOS-II虽然目录里有Ports和uCOS-II文件夹那是为后续升级预留的兼容接口。原因很实际小功率开环SPWM对实时性要求并不苛刻但对确定性要求极高。一旦引入RTOS任务切换带来的几微秒抖动就可能让两路互补PWM的边沿出现微妙偏移——在高压大电流下这种偏移就是直通短路的伏笔。因此主循环结构极其简单int main(void) { SystemInit(); // 系统时钟初始化HSE8MHz, PLL72MHz BSP_Init(); // 所有外设BSP层初始化GPIO/TIM/ADC/I2C/SPI/LCD/LED/KEY/BEEP PWM_Start(); // 启动TIM1输出初始占空比为0的SPWM LCD_ShowString(SPWM Ready); while(1) { Key_Scan(); // 非阻塞按键扫描消抖后置 Buzzer_Process(); // 蜂鸣器音效队列处理 LCD_Refresh(); // 液晶刷新仅更新变化区域 UART_Handle(); // 串口命令解析AT指令集 if (ADC_IsConversionDone()) { Vdc ADC_GetVoltage(); // 母线电压V Idc ADC_GetCurrent(); // 母线电流A PWM_UpdateDuty(Vdc, Idc); // 根据采样值动态调整保护阈值可选 } Delay_ms(10); // 主循环节拍非精确延时仅用于降低CPU占用 } }提示BSP_Init()是整套代码的基石。它不直接操作寄存器而是调用GPIO_Init(),TIM1_Init(),ADC_Init(),I2C1_Init()等封装好的函数。每个函数内部都做了严格的引脚复用检查比如确认PA8确实被配置为TIM1_CH1而非USART1_TX、时钟使能顺序先开RCC再配GPIO最后启外设、以及错误状态返回。这避免了新手常犯的“忘了开TIM1时钟结果PWM没输出还查半天”的低级错误。2.2 定时器选型铁律为什么必须是TIM1而不是TIM2/TIM3/TIM4STM32F10x系列有多个通用定时器但只有TIM1和TIM8是高级定时器具备以下不可替代的特性特性TIM1/TIM8其他通用定时器TIM2-5互补通道输出✅ 支持CH1/CH1N, CH2/CH2N, CH3/CH3N三组独立互补对❌ 仅支持普通PWM输出无N通道硬件死区插入✅ 可编程死区时间0~1008个时钟周期自动插入于上升沿/下降沿之间❌ 无此功能需软件模拟极不可靠刹车输入BKIN✅ 外部引脚PB12电平触发立即强制关闭所有互补通道输出❌ 无此功能重复计数器RCR✅ 支持多周期PWM生成简化SPWM载波同步❌ 无此功能我们来算一笔账假设载波频率为10kHz即周期100μsTIM1时钟为72MHz那么一个载波周期内计数器需计数72,000,000 / 10,000 7200次。若使用TIM1的向上计数模式自动重装载ARR则设置ARR 7199即可。此时每路PWM的占空比由捕获/比较寄存器CCR决定Duty CCR / (ARR 1)。但关键难点在于三相SPWM要求U、V、W三相彼此相差120°电角度。这意味着CH1、CH2、CH3的CCR值不能随意设定必须满足-CCR2 CCR1 (ARR1)/3-CCR3 CCR1 2*(ARR1)/3而TIM1的重复计数器RCR正是用来解决这个问题的——它可以设置一个“重复次数”让计数器在达到ARR后不立即归零而是继续计数RCR次后再清零。这使得我们可以在一个载波周期内通过改变CCR值生成多个不同相位的PWM边沿极大简化了正弦表查表逻辑。实操心得很多初学者试图用TIM2同时输出三路PWM然后用软件计算三个CCR值并分别写入。这在72MHz主频下看似可行但一旦加入ADC中断、串口接收等其他任务CCR写入时机就会漂移导致三相相位误差累积。而TIM1的RCR机制让相位关系由硬件计数器保证完全不受软件调度影响。这是我坚持用TIM1的最硬核理由。2.3 死区控制硬件插入 vs 软件延时生死只在100ns之间“死区时间”是防止上下桥臂直通的黄金法则。以IR2110驱动芯片为例其典型死区需求为500ns~2μs。如果靠软件延时实现// ❌ 危险绝对禁止的写法 TIM_SetCompare1(TIM1, ccr_u); Delay_us(1); // 假设这条指令耗时1μs TIM_SetCompare1N(TIM1, ccr_u_n);问题在于Delay_us(1)的实际耗时受编译器优化等级、中断抢占、流水线填充等多种因素影响误差可达±200ns。更致命的是这段代码本身不具备原子性——如果在Delay_us(1)执行中途来了个高优先级中断那死区就被彻底破坏了。正确做法是启用TIM1的硬件死区发生器Dead-Time Generator// ✅ 安全硬件级死区插入 TIM_BDTRInitStructure.TIM_OSSRState TIM_OSSRState_Enable; // 运行模式下保持输出 TIM_BDTRInitStructure.TIM_OSSIState TIM_OSSIState_Enable; // 空闲模式下保持输出 TIM_BDTRInitStructure.TIM_LOCKLevel TIM_LOCKLevel_1; // 锁定级别1防误写 TIM_BDTRInitStructure.TIM_DeadTime 120; // 死区时间 120 * Tdts TIM_BDTRInitStructure.TIM_Break TIM_Break_Enable; // 使能刹车 TIM_BDTRInitStructure.TIM_BreakPolarity TIM_BreakPolarity_High; // BKIN高电平有效 TIM_BDTRInitStructure.TIM_AutomaticOutput TIM_AutomaticOutput_Enable; TIM_BDTRConfig(TIM1, TIM_BDTRInitStructure);其中TIM_DeadTime 120是关键。它的单位不是纳秒而是TIM1时钟周期Tdts的倍数。由于TIM1时钟为72MHzTdts 13.89ns所以120 × 13.89ns ≈ 1.67μs完美匹配IR2110的推荐值。注意TIM_DeadTime的取值范围是0~255对应死区时间0~3.54μs。如果你的驱动芯片要求死区为500ns则应设为500 / 13.89 ≈ 36。切记不要凭感觉填“100”必须根据你的实际驱动IC手册计算。2.4 母线采样为什么ADC要放在PWM低电平中点为什么必须加RC滤波母线电压Vdc和电流Idc采样表面看只是两个ADC通道读数实则暗藏玄机。电压采样母线电压来自直流母排理论上平稳但开关噪声极大。IGBT每次开通/关断都会在母线上耦合出数百伏/微秒的尖峰dv/dt。如果ADC在任意时刻采样得到的可能是叠加了10V尖峰的“假电压”。电流采样通常采用单电阻采样Shunt Resistor位于下桥臂。当某相下管导通时电流流经采样电阻产生mV级压降。但此时上管关断瞬间的反向恢复电流、PCB走线电感都会在采样点引入强烈干扰。解决方案是同步采样Synchronized Sampling—— 将ADC触发源绑定到TIM1的特定事件上。在SPWM中每个载波周期内三相桥臂有明确的“上下管均关断”的时间段即PWM低电平期。此时开关噪声最小是采样的黄金窗口。我们选择在PWM信号的低电平中点触发ADC转换// 在TIM1初始化中配置更新事件UEV作为ADC触发源 TIM_SelectOutputTrigger(TIM1, TIM_TRGOSource_Update); // ADC配置使用外部触发触发源为TIM1_TRGO ADC_InitStructure.ADC_ExternalTrigConv ADC_ExternalTrigConv_T1_TRGO; ADC_InitStructure.ADC_DataAlign ADC_DataAlign_Right; ADC_InitStructure.ADC_NbrOfChannel 2; ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 1, ADC_SampleTime_55Cycles5); // Vdc ADC_RegularChannelConfig(ADC1, ADC_Channel_2, 2, ADC_SampleTime_55Cycles5); // Idc同时硬件上必须添加两级滤波-一级RC低通在ADC输入引脚前放置R1kΩ C100nF截止频率≈1.6kHz滤除高频开关噪声-二级数字滤波在软件中对连续16次ADC采样值进行中值滤波 滑动平均剔除异常脉冲。实操心得我曾遇到一块板子Vdc读数始终比万用表低2V。排查三天最终发现是PCB上Vdc采样点离母线电容太远走线电感导致压降。后来在采样点就近并联一个10μF陶瓷电容读数立刻准确。这提醒我们ADC精度不仅取决于代码更取决于硬件布局。固件里写的“采样中点触发”是给硬件工程师的明确设计约束——你的RC滤波电容必须放在离MCU ADC引脚最近的位置。3. 核心模块详解与实操要点从main.c配置到.hex烧录每一步都是经验之谈3.1 main.c三行宏定义决定整个驱动行为main.c是整个项目的“总控台”。所有可配置参数都集中在这里的宏定义中。修改它们无需动底层驱动即可适配不同电机、不同驱动板。// SPWM核心参数 #define SPWM_CARRIER_FREQ_KHZ (10) // 载波频率10kHz建议范围5~20kHz #define SPWM_MODULATION_INDEX (0.85f) // 调制比0.85最大0.99超过会削顶 #define SPWM_DEAD_TIME_NS (1670) // 死区时间1670ns对应TIM_DeadTime120 // 硬件资源映射 #define KEY_GPIO_PORT GPIOA #define KEY_GPIO_PIN GPIO_Pin_0 // 按键接PA0 #define BUZZER_GPIO_PORT GPIOB #define BUZZER_GPIO_PIN GPIO_Pin_5 // 蜂鸣器接PB5 #define LCD_SPI_PORT SPI1 // ST7565使用SPI1 #define EEPROM_I2C_PORT I2C1 // 24C02使用I2C1 // ADC采样配置 #define ADC_VDC_CHANNEL ADC_Channel_1 // Vdc接PA1ADC1_IN1 #define ADC_IDC_CHANNEL ADC_Channel_2 // Idc接PA2ADC1_IN2 #define ADC_VDC_REF_VOLTAGE (3.3f) // ADC参考电压3.3V #define ADC_IDC_SHUNT_RESISTOR (0.01f) // 采样电阻0.01Ω #define ADC_IDC_AMPLIFIER_GAIN (10.0f) // 电流运放增益10倍这些宏定义背后是大量硬件适配经验SPWM_CARRIER_FREQ_KHZ载波频率不是越高越好。10kHz是平衡点——高于15kHz人耳听不到噪音但开关损耗剧增低于5kHz电机噪音刺耳且转矩脉动明显。我测试过20kHz散热片温度比10kHz高12℃而噪音改善微乎其微。SPWM_MODULATION_INDEX调制比决定了最大输出电压。理论最大值为1但实际中因死区、驱动延迟、器件非理想性超过0.9后SPWM波形开始削顶THD急剧上升。0.85是兼顾输出能力和波形质量的安全值。SPWM_DEAD_TIME_NS这个值必须与你的驱动芯片手册严格对照。例如英飞凌的IR2110推荐死区≥500ns而TI的UCC2732X要求≥100ns。填错会导致要么直通炸管要么输出电压严重损失。提示ADC_IDC_AMPLIFIER_GAIN这个参数极易被忽略。很多开发者直接把运放输出接到ADC却不标定增益。结果是代码里写Idc adc_val * 3.3 / 4096 * 10 / 0.01但实际运放电路可能因为电阻公差真实增益是9.8或10.3。我的建议是在eeprom.c中预留一个校准系数g_adc_idc_gain_cal首次上电时用标准电流源标定一次存入24C02后续直接读取。这比每次改代码靠谱得多。3.2 PWM生成正弦表、查表逻辑与TIM1寄存器配置全解析SPWM的本质是用一系列等幅不等宽的矩形脉冲去逼近正弦波。核心是正弦表Sine Table。本固件采用256点正弦表存储在Flash中const uint16_t sine_table[256] { 2048, 2099, 2150, 2201, 2252, 2302, 2352, 2402, // ... 256个值 };每个值范围0~4095对应ADC的12位分辨率。表中第0点为sin(0°)0第64点为sin(90°)1 → 4095第128点为sin(180°)0依此类推。生成三相SPWM的流程如下相位偏移计算U相取表索引idxV相取idx 85256×120°/360°≈85W相取idx 171256×240°/360°≈171查表获取幅值u_amp sine_table[idx 0xFF]v_amp sine_table[(idx85) 0xFF]w_amp sine_table[(idx171) 0xFF]映射为占空比duty_u (uint16_t)(u_amp * modulation_index)同理v、w写入TIM1 CCR寄存器TIM_SetCompare1(TIM1, duty_u)TIM_SetCompare2(TIM1, duty_v)TIM_SetCompare3(TIM1, duty_w)索引递增idx进入下一载波周期。关键点在于索引递增必须与TIM1的更新事件Update Event严格同步。否则正弦表步进节奏紊乱输出就是一堆杂波。// 在TIM1中断服务函数中TIM1_UP_IRQHandler void TIM1_UP_IRQHandler(void) { if (TIM_GetITStatus(TIM1, TIM_IT_Update) ! RESET) { TIM_ClearITPendingBit(TIM1, TIM_IT_Update); // 更新SPWM占空比 idx; duty_u (uint16_t)(sine_table[idx 0xFF] * g_modulation_index); duty_v (uint16_t)(sine_table[(idx85) 0xFF] * g_modulation_index); duty_w (uint16_t)(sine_table[(idx171) 0xFF] * g_modulation_index); TIM_SetCompare1(TIM1, duty_u); TIM_SetCompare2(TIM1, duty_v); TIM_SetCompare3(TIM1, duty_w); } }注意idx 0xFF是为了确保索引在0~255范围内循环避免数组越界。而g_modulation_index是一个全局浮点变量其值可由串口AT指令动态修改实现在线调速。3.3 母线采样与保护ADC双通道同步、数字滤波与保护阈值联动ADC模块配置是本固件的另一大亮点。它实现了单次触发、双通道同步采样、硬件过采样Oversampling。// ADC初始化关键配置 ADC_DeInit(ADC1); ADC_StructInit(ADC_InitStructure); ADC_InitStructure.ADC_Mode ADC_Mode_Independent; // 独立模式 ADC_InitStructure.ADC_ScanConvMode ENABLE; // 扫描模式多通道 ADC_InitStructure.ADC_ContinuousConvMode DISABLE; // 单次转换由TIM1触发 ADC_InitStructure.ADC_ExternalTrigConv ADC_ExternalTrigConv_T1_TRGO; // 外部触发 ADC_InitStructure.ADC_DataAlign ADC_DataAlign_Right; ADC_InitStructure.ADC_NbrOfChannel 2; ADC_Init(ADC1, ADC_InitStructure); // 配置规则组通道1Vdc、通道2Idc ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 1, ADC_SampleTime_55Cycles5); ADC_RegularChannelConfig(ADC1, ADC_Channel_2, 2, ADC_SampleTime_55Cycles5); // 使能ADC1和ADC1的DMA可选此处未用DMA用查询方式 ADC_Cmd(ADC1, ENABLE); ADC_ResetCalibration(ADC1); while(ADC_GetResetCalibrationStatus(ADC1)); ADC_StartCalibration(ADC1); while(ADC_GetCalibrationStatus(ADC1));采样后的数据处理采用三级滤波硬件RC滤波如前所述R1kΩ C100nF软件中值滤波缓存最近16次采样值排序后取第8个滑动平均滤波对中值滤波结果再做8点滑动平均。// 滑动平均滤波器伪代码 static float vdc_filter_buf[8] {0}; static uint8_t vdc_filter_idx 0; float vdc_filtered 0; vdc_filter_buf[vdc_filter_idx] vdc_raw; vdc_filter_idx (vdc_filter_idx 1) % 8; for(int i0; i8; i) vdc_filtered vdc_filter_buf[i]; vdc_filtered / 8.0f;保护逻辑直接与采样值联动if (vdc_filtered 35.0f) { // 母线过压35V PWM_Stop(); // 立即停止PWM输出 LED_RedOn(); Buzzer_Alert(2); // 长鸣2声 return; } if (idc_filtered 8.5f) { // 母线过流8.5A PWM_Stop(); LED_YellowBlink(5); // 黄灯快闪5次 Buzzer_Alert(1); return; }实操心得保护阈值绝不能写死在代码里必须支持掉电保存。我在eeprom.c中定义了结构体c typedef struct { float vdc_over_voltage; // 过压阈值 float idc_over_current; // 过流阈值 float modulation_index; // 当前调制比 uint16_t dead_time_ns; // 当前死区时间 } t_motor_params;上电时从24C02读取修改后调用EEPROM_WriteParams(params)保存。这样客户在现场调试时只需发ATVDCOV36.5就能永久修改过压阈值无需重新编译烧录。3.4 参数掉电保存24C02与片内Flash双备份谁才是真正的“永不丢失”参数保存看似简单实则关乎产品可靠性。本固件采用24C02I2C接口EEPROM为主存储STM32F10x片内Flash为备份存储的双保险策略。为什么不用Flash单独存储- Flash擦写寿命仅10,000次而24C02可达1,000,000次- Flash擦除以页1KB为单位写入前必须先擦除整页效率低下- 频繁写Flash易导致Bootloader区意外损坏。但24C02也有软肋I2C总线受干扰可能导致写入失败且掉电瞬间若恰逢写入过程数据可能损坏。因此固件设计为-日常运行所有参数读写均操作24C02-上电自检先读24C02若校验失败CRC16不匹配则从Flash备份区恢复-写入保障每次写24C02前先将新参数写入Flash备份区待24C02写入成功后再更新Flash中的“有效标志”。// 写入流程简化 bool EEPROM_WriteParams(t_motor_params* p) { // Step 1: 写入Flash备份区地址0x0800F000 FLASH_Unlock(); FLASH_ErasePage(FLASH_PAGE_240); // 擦除第240页 FLASH_ProgramHalfWord(FLASH_ADDR_BACKUP, (uint16_t)((uint32_t)p 0xFFFF)); FLASH_ProgramHalfWord(FLASH_ADDR_BACKUP2, (uint16_t)(((uint32_t)p 16) 0xFFFF)); FLASH_Lock(); // Step 2: 写入24C02地址0x00 if (I2C_WriteBuffer(EEPROM_ADDR, 0x00, (uint8_t*)p, sizeof(t_motor_params)) SUCCESS) { // Step 3: 写入Flash中的valid flag FLASH_ProgramHalfWord(FLASH_ADDR_VALID_FLAG, 0xAA55); return true; } return false; }提示24C02的I2C地址通常是0x50A0A1A20。但务必用万用表实测你的PCB上A0~A2引脚的电平我曾帮客户调试发现他们把A0焊错了导致I2C通信一直超时折腾两天才找到原因。固件里24c02.h中定义的#define EEPROM_ADDR 0xA0是写地址含R/W位实际读写时需右移1位即0x50。3.5 外设驱动封装为什么led.c里要区分LED_On()和LED_Toggle()所有外设驱动led.c,key.c,spk.c,st7565.c,24c02.c都遵循同一套封装哲学隐藏硬件细节暴露业务语义。以LED为例// led.h void LED_Init(void); // 初始化所有LED引脚为推挽输出 void LED_RedOn(void); // 红灯常亮表示故障 void LED_RedOff(void); // 红灯熄灭 void LED_GreenBlink(uint8_t n); // 绿灯闪烁n次表示成功 void LED_YellowBlink(uint8_t n); // 黄灯闪烁n次表示警告 void LED_AllOff(void); // 所有LED熄灭 // led.c 实现 void LED_RedOn(void) { GPIO_ResetBits(LED_RED_GPIO_PORT, LED_RED_GPIO_PIN); // 低电平点亮共阳接法 } void LED_GreenBlink(uint8_t n) { for(uint8_t i0; in; i) { GPIO_SetBits(LED_GREEN_GPIO_PORT, LED_GREEN_GPIO_PIN); Delay_ms(100); GPIO_ResetBits(LED_GREEN_GPIO_PORT, LED_GREEN_GPIO_PIN); Delay_ms(100); } }注意LED_RedOn()和LED_GreenBlink()的实现逻辑完全不同。前者是状态设置后者是时序动作。这种区分让main.c的业务逻辑无比清晰if (motor_start_success) { LED_GreenBlink(1); // 启动成功绿灯闪1次 } else { LED_RedOn(); // 启动失败红灯常亮 }而不是一堆GPIO_SetBits()、GPIO_ResetBits()混在业务逻辑里让人无法一眼看出意图。同样st7565.c不提供SPI_WriteByte()而是提供LCD_DrawPixel(),LCD_DrawLine(),LCD_ShowString()serial.c不暴露USART_SendData()而是提供UART_Printf(),UART_ATCommandHandler()。这种封装让一个刚接手项目的工程师能在10分钟内看懂整个系统的交互流程。4. 实操全流程从环境搭建到烧录验证一份不漏的“抄作业指南”4.1 开发环境准备Keil MDK-ARM v5.27不是最新版但最稳本固件基于Keil MDK-ARM v5.27开发配套ARM Compiler v5.06 update 6 (build 750)。为什么不推荐最新版因为v5.27对STM32F10x标准外设库StdPeriph_Lib兼容性最好且生成的代码体积最小这对64KB Flash的F103RB至关重要。安装步骤下载并安装 Keil MDK-ARM v5.27官网可找到历史版本安装 ARM Compiler v5.06 update 6安装包内自带将STM32LIB文件夹复制到Keil安装目录下的ARM\PACK\Keil\STM32F1xx_DFP\2.3.0\Device\Source\Templates\arm\路径可能略有不同请按实际调整打开stmucos.uvproj工程文件注意虽然工程名含uCOS但默认配置为裸机模式在Project - Options for Target - Device中确认芯片型号为STM32F103RCT6或你实际使用的型号在C/C选项卡中确保Define包含USE_STDPERIPH_DRIVER, STM32F10X_MDMD表示中密度Flash 64KB在Output选项卡中勾选Create HEX File输出路径为out_hex\。提示如果编译报错undefined symbol RCC_APB2Periph_GPIOA说明Define中漏了STM32F10X_MD。这是新手最高频错误。4.2 硬件连接核查一张表搞定所有引脚映射在烧录前务必对照下表用万用表蜂鸣档逐个测量你的PCB功能MCU引脚说明必测TIM1_CH1PA8U相上桥臂驱动信号✅TIM1_CH1NPA7U相下桥臂驱动信号✅TIM1_CH2PA9V相上桥臂驱动信号✅TIM1_CH2NPB0V相下桥臂驱动信号✅TIM1_CH3PA10W相上桥臂驱动信号✅TIM1_CH3NPB1W相下桥臂驱动信号✅ADC_VDCPA1母线电压分压后接入✅ADC_IDCPA2电流采样电阻后接入✅I2C_SCLPB624C02时钟线✅I2C_SDAPB724C02数据线✅SPI_SCKPA5ST7565时钟线✅SPI_MOSIPA7ST7565数据线注意与TIM1_CH1N复用⚠️LCD_CSPA4ST7565片选✅LCD_RSTPA3ST7565复位✅KEYPA0按键输入上拉✅BUZZERPB5蜂鸣器驱动NPN三极管✅⚠️ 重点警告PA7同时是TIM1_CH1N和SPI_MOSI。本固件中ST7565使用软件SPIBit-Banging即不启用SPI外设而是用GPIO模拟时序。因此PA7被配置为GPIO_Mode_Out_PP用于TIM1输出液晶显示由PB12/PB13/PB14/PB15四根GPIO模拟SPI实现。如果你的PCB已将PA7硬连到液晶必须修改st7565.c中的引脚定义并重新分配TIM1_CH1N到其他支持复用的引脚如PB13需查STM32F103数据手册确认。4.3 编译与烧录J-Link Commander一行命令比Keil GUI更快编译成功后out_hex\目录下会生成stmucos.hex。烧录推荐使用J-Link Commander比Keil GUI更可靠尤其对老旧J-Link固件# 打开J-Link CommanderWindows下运行JLink.exe J-Link connect Please specify device family [ARM/Cortex-M]: ARM Specify target interface [JTAG/SWD]: SWD Specify target interface speed [kHz]: 4000 Connect to device via SWD. Found SW-DP with ID 0x1BA01477 Found SW-DP with ID 0x1BA01477 Found Cortex-M3 r1p1, Little endian. FPUnit: 6 code (BP) slots and 2 literal slots CoreSight components: ROMTbl[0] E00FF000 ROMTbl[0][0] E000E000: CID B105E00D, PID 000BB00C SCS-M3 ROMTbl[0][1] E0001000: CID B105E00D, PID 000BB002 DWT ROMTbl[0][2] E0002000: CID B105E00D, PID 000BB003 FPB ROMTbl[0][3] E0000000: CID B105E00D, PID 000BB001 ITM ROMTbl[0][4] E0040000: CID B105E00D, PID 000BB006 TPIU ROMTbl[0][5] E0041000: CID B105E00D, PID 000BB007 ETM J-Link loadfile out_hex\stmucos.hex Downloading file [out_hex\stmucos.hex]... Comparing flash areas ... Erasing sectors Done. Programming flash ... Verifying flash ... J-Link r J-Link g J-Link exit提示如果烧录失败首先检查J-Link的SWDIO/SWCLK线是否虚焊其次在Project - Options for Target - Debug中确认Use选择了J-Link/J-Trace且Settings中Interface为SWDSpeed为4000 kHz。4.4 首次上电调试五步法快速定位90%的问题板子第一次上电不要急着接电机。按以下五步排查看电源用万用表测3.3V和5V是否稳定。若3.3V跌至3.0V以下说明LDO过载或电容失效看LED上电后所有LED应短暂全亮LED_AllOff()在BSP_Init()末尾执行故初始为全亮然后红灯常亮系统初始化中默认设为故障态。若LED不亮检查LED限流电阻和GPIO配置听蜂鸣器上电瞬间应有1声短鸣Buzzer_Alert(1)在main()开头调用。若无声检查蜂鸣器驱动三极管和PB5配置看液晶ST7565应显示SPWM Ready。若黑屏重点查LCD_RSTPA3是否被拉低、LCD_CSPA4是否正常、SPI模拟引脚电平串口通信用USB-TTL模块接PA9(TX)/PA10(RX)波特率115200发送AT应回复OK。若无响应检查串口引脚是否与TIM1_CH2/CH3冲突本固件中串口使用USART1TXPA9, RXPA10而TIM1_CH2PA9TIM1_CH3PA10 ——这是严重冲突。⚠️ 致命冲突预警本固件中TIM1_CH2和USART1_TX都映射到PA9TIM1_CH3和USART1_RX都映射到PA10。这是硬件设计硬伤解决方案有两个-推荐将串口改为USART2PA2TX,PA3RX修改serial.c中的USARTx定义和引脚初始化-次选将TIM1_CH2/CH3重映射到其他引脚如PB13/PB14需修改TIM1_Init()中的GPIO_PinRemapConfig()并更新PCB。这一步排查完你的板子就活了。接下来接上电机慢慢调高调制比用示波器看U/V/W三相波形——你会看到教科书般的SPWM干净、对称、死区清晰。5. 常见问题与排查技巧实录那些让我熬夜到凌晨三点的Bug5.1 典型问题速查表现象可能原因排查步骤解决方案PWM无输出示波器测不到任何波形① TIM1时钟未使能② GPIO复用功能未开启③ ARR值为0或过大④ 输出比较使能未打开① 检查RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1, ENABLE)② 检查GPIO_PinAFConfig(GPIOA, GPIO_PinSource8, GPIO_AF_TIM1)③ 检查TIM_SetAutoreload(TIM1, 7199)④ 检查TIM_CCxCmd(TIM1, TIM_Channel_1, TIM_CCx_Enable)在TIM1_Init()函数末尾添加TIM_Cmd(TIM1, ENABLE)和TIM_CtrlPWMOutputs(TIM1, ENABLE)缺一不可三相波形相位不对不是120°① 正弦表索引计算错误② 查表时未做模运算idx溢出③ TIM1更新中断未使能① 检查idx 0xFF是否遗漏② 检查sine_table[(idx85) 0xFF]中的85是否应为85或84256×120/36085.33→取整③ 检查TIM_ITConfig(TIM1, TIM_IT_Update, ENABLE)使用调试器单步执行中断服务函数观察idx变量是否规律递增duty_u/v/w是否符合正弦规律母线电压读数跳变剧烈±5V① 硬件RC滤波缺失或参数错误② ADC触发源未同步到PWM低电平③ 电源纹波过大① 用示波器测PA1引脚确认是否有高频噪声② 检查TIM_SelectOutputTrigger(TIM1, TIM_TRGOSource_Update)是否配置③ 测3.3V电源纹波若50mV增加100μF电解电容在ADC_Init()后添加ADC_RegularChannelConfig()前插入ADC_SoftwareStartConvCmd(ADC1, DISABLE)确保无软件触发干扰24C02写入失败I2C通信超时① A0~A2地址引脚电平错误② 上拉电阻过大10kΩ或缺失③ SCL/SDA线过长或受干扰① 用万用表测PB6/PB7对地电压应为3.3V上拉② 检查PCB上是否焊接了4.7kΩ上拉电阻③ 缩短SCL/SDA走线远离PWM走线在I2C_WriteBuffer()函数中增加超时计数器避免死循环并在超时后打印I2C_ERROR日志5.2 独家避坑技巧来自产线的血泪教训技巧1用LED做“逻辑分析仪”示波器不是随时都有。我习惯在关键节点插入LED指示-LED_BlueOn()放在TIM1_UP_IRQHandler开头LED_BlueOff()放在结尾。这样用肉眼就能判断中断是否在运行、频率是否正确10kHz时蓝灯是肉眼不可分辨的持续亮。-LED_YellowOn()放在ADC_IsConversionDone()返回true时可直观看到ADC采样是否触发。技巧2串口日志分级不拖慢主循环不要在主循环里printf(Vdc%f\n, vdc)。这会让115200波特率的串口成为瓶颈。我的做法是- 定义环形缓冲区char uart_log_buf[256]- 所有日志用snprintf()格式化后写入缓冲区- 在UART_Handle()中每次只发送16字节分多次发完- 关键错误如过压用UART_ForceSend()立即发送牺牲一点实时性换取可靠性。技巧3“一键恢复出厂设置”物理按键在PCB上设计一个双击按键KEY第一次按下黄灯慢闪第二次在2秒内按下执行EEPROM_EraseAll()并恢复默认参数。这比用串口发10条AT指令快得多产线工人最爱。技巧4烧录后首件事——测TIM1_CH1N与CH1的死区用示波器同时测PA8CH1和PA7CH1N。正常情况是CH1下降沿后CH1N上升沿延迟约1.67μs。若两者紧挨着甚至重叠说明TIM_DeadTime设置为0或TIM_BDTRConfig()未调用。立刻断电检查代码。最后分享一个小技巧这个固件的.hex文件我习惯用hex2bin转成二进制然后用xxd -g1 firmware.bin | head -20查看前20行。你会发现0x08000000地址处是0x00 0x00 0x00 0x20栈顶地址0x08000004处是0x21 0x01 0x00 0x08复位向量指向0x08000121。这能帮你快速确认烧录是否完整——如果0x08000004是0xFF说明烧录失败。这个技巧救过我三次。这套代码从第一行#include stm32f10x.h写起到最终在客户产线上稳定运行两年中间删掉了37个TODO注释重写了5版PWM_UpdateDuty()函数更换了4种不同的IGBT驱动芯片。它不完美但足够可靠它不炫技但直击痛点。如果你正站在一块空白的三相驱动板前希望它能在最短时间内转起来——那就放心地把它当成你的起点。本文还有配套的精品资源点击获取简介专为STM32F10x系列设计的三相电机开环驱动固件包基于TIM1高级定时器生成六路带可调死区的SPWM波形支持IGBT半桥驱动逻辑内置ADC模块实时采集母线电压与电流信号配合EEPROM24C02和片内Flash实现运行参数如载波频率、调制比、死区时间掉电保存提供ST7565液晶显示驱动、LED状态指示、串口调试通信含serial.h/c、蜂鸣器与按键交互功能所有外设驱动GPIO、TIMER、ADC、I2C、SPI已完成底层封装main.c中仅需修改宏定义即可快速适配不同硬件参数编译输出标准.hex文件可直接通过J-Link等工具烧录适用于小功率三相异步电机或永磁同步电机的SPWM开环控制场景目录结构清晰包含BSP层、uCOS-II移植支持、启动文件、驱动源码及文档资源。本文还有配套的精品资源点击获取