
前阵子帮朋友收拾一个基于STM32F103C8T6的小项目固件写到最后发现Flash只剩几百字节想加功能没地方放想外扩串口引脚也占满了。朋友问能不能换成STM32F103RCT6我说可以而且Keil工程不用重写。结果他回去自己试了半小时回来跟我说报错程序一运行就进HardFault。我远程一看启动文件还是md.s宏定义还是STM32F10X_MD下载算法也是中密度的64K。典型的“只改了Device换来一个跑不起来的工程”。这篇文章就是把“怎么改”和“为什么这么改”一次讲透。适用对象很明确手里有C8T6的工程想换到RCT6继续用的开发者不管是标准外设库还是HAL库重点说Keil工程里那几处决定生死的配置。只要按这套流程走完原来C8T6上的外设代码基本能做到零改动迁移新芯片多出来的Flash、SRAM、串口、定时器都能直接用上。1. 换芯片之前先看懂C8T6与RCT6的“密度”差异1.1 一张参数表说清两个芯片的差异很多人以为C8T6和RCT6只是Flash和引脚数量不同换过去把Device一改就完事。实际上两个芯片在STM32家族里分属不同密度等级C8T6是Medium-density中密度RCT6是High-density高密度。密度等级决定了启动文件、预处理宏和中断向量表这才是迁移的核心。对比项STM32F103C8T6STM32F103RCT6官方Flash64KB256KBSRAM20KB48KB封装/引脚LQFP48LQFP64GPIO数量约37个PA/PB/PC13-15/PD0-1约51个多出完整PC、PD、PE口串口USART1/2/3USART1/2/3 UART4/UART5SPISPI1/SPI2SPI1/SPI2/SPI3I2CI2C1/I2C2I2C1/I2C2定时器TIM1/TIM2/TIM3/TIM4TIM1-TIM8新增TIM5/6/7/8ADCADC1/ADC2ADC1/ADC2/ADC3密度分类Medium-densityHigh-densityKeil启动文件startup_stm32f10x_md.sstartup_stm32f10x_hd.s标准库预处理宏STM32F10X_MDSTM32F10X_HDFlash下载算法Med-density Flash 64KHigh-density Flash 256K这张表里最关键的是倒数三行启动文件、预处理宏、Flash下载算法。它们共同决定了工程能否正确认识这颗芯片。这三样不配套哪怕代码写得再对编译能过实际运行也会莫名跑飞。1.2 为什么“密度分类”决定了工程配置而不是单纯看Flash大小把一个C8T6的Keil工程打开在Options for Target里把Device从STM32F103C8改成STM32F103RCMDK确实会帮你把Target页的存储区域刷新成256KB Flash和48KB SRAM。但MDK不会自动替换你工程里的启动文件也不会自动修改预处理宏。先说启动文件。startup_stm32f10x_md.s和startup_stm32f10x_hd.s最核心的区别在于中断向量表长度。中密度芯片只有43个左右的可屏蔽中断源高密度芯片却有60个。举个直观的例子RCT6的TIM5中断在md.s的向量表里根本没有对应的入口地址。一旦程序里使能了这个中断中断触发时CPU去向量表找入口找到的是个无效地址或者Default_Handler结果就是直接进HardFault。我见过不少“换芯片后偶尔死机”的案例最后查下来都是这个原因。再说预处理宏。标准外设库的stm32f10x.h里大量使用条件编译#if defined(STM32F10X_HD) || defined(STM32F10X_CL) #define TIM8 ((TIM_TypeDef *)TIM8_BASE) #define UART4 ((USART_TypeDef *)UART4_BASE) #endif如果宏定义还是STM32F10X_MD代码里访问TIM8、UART4、SPI3这些寄存器结构体时编译器直接报未定义。即便你运气好没用这些新增外设库函数里某些与DMA通道、ADC3相关的实现也会因为宏不对而编译出与RCT6硬件不匹配的版本。1.3 换RCT6的收益和容易忽略的板级风险换芯片最直接的收益是容量翻了几倍Flash从64KB到256KBSRAM从20KB到48KB。C8T6官方标称64KB Flash实际市场上很多批次芯片内部是128KB不少人卡着这个灰产标准跑到80KB以上但正规产品不建议这么冒险。换到RCT6后256KB的空间足够跑完整RTOS、GUI、多路通信协议栈SRAM也从“抠抠搜搜”变得相对从容。引脚方面RCT6多出完整的PC0-PC15、PD0-PD15、PE0-PE15。原来C8T6上为了一个GPIO反复调整复用功能的憋屈到RCT6基本不存在了。同时新增UART4/UART5、SPI3、TIM5-TIM8、ADC3等于在模块化设计时多了好几条路。但板级风险要提前评估。C8T6是LQFP48封装RCT6是LQFP64封装两者引脚不兼容PCB必须重新画不能直接替换焊接。另外RCT6的LQFP64封装在PD0/PD1上同样和HSE晶振复用PC14/PC15和LSE晶振复用如果你新画的板子外接了这两颗晶振对应的引脚就不能当普通IO用了。BOOT0、BOOT1的上下拉电路也要复核尤其是PB2作为BOOT1引脚上电瞬间的电平会决定芯片从哪启动。2. Keil迁移的四步操作缺一步都容易返工2.1 第一步修改Target Device为STM32F103RC打开工程后按AltF7进入Options for Target在Device页左侧导航找到STMicroelectronics - STM32F1 Series - STM32F103选中STM32F103RC。这里想说个细节MDK会提示所需Pack一般你原来装好STM32F1xx_DFP就能识别不需要额外下载。如果你用的是老版本MDK比如MDK 4.x需要手动安装支持STM32F103RC的Device Pack否则Device列表里根本找不到这颗芯片。改完Device后Target页里的IROM1和IRAM1通常会被MDK自动刷新但这一步不能完全信任自动配置后面第四步会单独核验。2.2 第二步把启动文件从md.s换成hd.s这一步是“无缝转换”的重中之重。在MDK左侧Project栏找到startup_stm32f10x_md.s右键选择Remove from File Group把它移除出工程。然后在工程组里右键Add Existing Files从标准外设库的启动文件目录里加入startup_stm32f10x_hd.s。标准外设库V3.5的路径一般是STM32F10x_StdPeriph_Lib_V3.5.0\Libraries\CMSIS\CM3\DeviceSupport\ST\STM32F10x\startup\arm\加入后还有一个小坑新加的.s文件默认不一定参与编译。右键这个文件选择Options for File确保“Include in Target Build”前面打了勾否则这个启动文件等于没加。判断它有没有被编译链接可以编译后看Build Output里的链接信息或者直接看.map文件里有没有Stack_Size、Reset_Handler这些符号。2.3 第三步改预处理宏STM32F10X_MD为STM32F10X_HD在Options for Target - C/C - Preprocessor Symbols - Define一栏原来写的是STM32F10X_MD,USE_STDPERIPH_DRIVER改成STM32F10X_HD,USE_STDPERIPH_DRIVERUSE_STDPERIPH_DRIVER这个宏是标准外设库的“开关”告诉库文件要包含stm32f10x_conf.h并启用外设驱动函数这个保留不动的意思。而STM32F10X_MD改为STM32F10X_HD直接影响stm32f10x.h里对高密度外设的判断。如果工程里用的是寄存器操作方式没有使用标准外设库那这个宏对你的影响会小很多但仍建议把它改对因为你无法保证后续新增代码不会依赖库里的条件编译。2.4 第四步同步检查Target存储区与Flash下载算法打开Options for Target - Target页确认Memory区域配置与RCT6一致IROM1Start0x08000000Size0x40000勾选Read/OnlyIRAM1Start0x20000000Size0xC000勾选Read/Write如果原来的工程手动改过存储地址这里会被覆盖成旧值必须改回来。C8T6的IRAM1 Size是0x5000也就是20KB不改成0xC000的话工程会以为SRAM只有20KB超过20KB的变量直接链接报错。然后看Debug页。选择你使用的调试器比如ST-Link或J-Link点击旁边的Settings在Flash Download选项卡里清掉原来的Med-density算法STM32F10x Med-density Flash 64K添加RCT6对应的算法STM32F10x High-density Flash 256K如果没有这个算法选项检查你安装的Pack是不是完整版或者手动从MDK安装目录的ARM/Flash文件夹里添加对应的.FLM文件。这个算法的本质是烧录器用来擦除和写Flash的驱动程序算法与芯片容量不匹配时轻则下载报错重则烧录后程序不运行。2.5 给CubeMX/HAL库用户的补充说明如果你的工程是CubeMX HAL库生成的流程其实更简单打开.ioc文件在芯片选择界面搜索STM32F103RCT6点击MigrateCubeMX会重新生成整个工程。重新生成后的Keil工程里Device、启动文件、HAL库配置会自动对齐基本不需要手动干预。但要注意HAL库工程的启动文件名是startup_stm32f103xe.s不是标准库的hd.s这是HAL库自己的命名规则和ST官方Cube包配套。所以如果你拿到一个别人给的HAL库工程不要盲目去找startup_stm32f10x_hd.s优先用CubeMX重新生成才是正道。3. 代码层面保持一套工程兼容两块芯片3.1 为什么原有代码大多可以不动C8T6的代码量一般不会超过64KB这些代码烧进RCT6后地址仍然落在0x08000000开始的低64KB区域。RCT6是向下兼容的所以Flash容量差异本身不会导致老代码出问题。真正会出问题的地方还是中断向量表和外设使能。只要第2章的启动文件和宏定义都改对了原来C8T6上跑通的USART1、SPI1、I2C1、TIM2这些外设代码在RCT6上完全不用动。因为它们对应的寄存器地址、引脚编号、时钟使能位在C8T6和RCT6之间是一模一样的。3.2 用条件编译做板级配置切换如果你手头有多块板子或者希望工程文件能在C8T6和RCT6之间来回切换推荐用条件编译写一套板级配置。标准外设库在编译时已经定义了STM32F10X_HD这个宏可以直接拿来做判断。举个例子C8T6最小系统板上的LED通常接在PC13而RCT6开发板的LED可能在PD2或者PE5。可以这样写#if defined(STM32F10X_HD) #define LED_RCC RCC_APB2Periph_GPIOD #define LED_PORT GPIOD #define LED_PIN GPIO_Pin_2 #else #define LED_RCC RCC_APB2Periph_GPIOC #define LED_PORT GPIOC #define LED_PIN GPIO_Pin_13 #endif static void LED_GPIO_Config(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(LED_RCC, ENABLE); GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Pin LED_PIN; GPIO_Init(LED_PORT, GPIO_InitStructure); }这样同一套代码编译C8T6时用的是PC13编译RCT6时用的是PD2不需要每次迁移都去改引脚定义。新增外设也可以用类似方式#if defined(STM32F10X_HD) UART4_Config(); SPI3_Config(); TIM5_Config(); #endif这种写法在维护多个硬件版本时非常省心但前提是第2章里讲的预处理宏一定得改对。宏不对#if defined(STM32F10X_HD)这段代码直接不会被编译进工程到时候板子不工作还找不到原因。3.3 特殊引脚和复用功能换到RCT6后更容易踩坑换到RCT6后外设资源变多了很多在C8T6上“不敢用”的引脚开发者会忍不住去碰。有几个位置需要提前知道第一个是PB2也就是BOOT1引脚。很多人看到RCT6多出来的PB口就想用但PB2在芯片上电采样阶段会被当作BOOT1信号。如果BOOT0此时为高电平BOOT1也为高芯片会进入系统存储器Bootloader你的用户程序根本启动不起来。C8T6同样有这个限制只是引脚少的时候大家不太会去用PB2到了RCT6有了富余引脚这个坑就很容易踩到。解决办法是PB2当普通IO用的时候板子上要确保上电瞬间BOOT0为低或者BOOT1的下拉电阻足够强让它默认低电平。第二个是PA13、PA14、PA15、PB3、PB4这五个引脚。它们在复位后默认是SWD/JTAG调试口PA13是SWDIOPA14是SWCLKPA15是JTDIPB3是JTDOPB4是NJTRST。如果要把PA15、PB3、PB4当普通IO用必须在初始化时关闭JTAG功能但保留SWDGPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE);这句配置的意义是禁用JTAG调试接口释放PA15/PB3/PB4给GPIO使用同时保留SWD的两个引脚所以程序烧录和调试不受影响。这个操作在C8T6和RCT6上写法一致但RCT6的引脚更多用到的频率更高。第三个是PC14、PC15与PD0、PD1。PC14、PC15在芯片内部与LSE低速外部晶振引脚复用PD0、PD1与HSE高速外部晶振引脚复用。如果你的板子上没有焊接对应晶振并且软件里关闭了HSE、LSE时钟这四个引脚可以当普通IO用如果板子上有晶振或者你想使用RTC依赖LSE那PC14/PC15就不要碰PD0/PD1同理。4. 迁移后容易翻车的三个典型问题与排查链路4.1 案例程序一运行就进HardFault这是C8T6换RCT6最经典的翻车现场。现象是MDK编译零错误零警告用ST-Link下载成功复位后程序却跑不起来在调试器里点Run马上停在HardFault_Handler。标准外设库工程如果只改了Device没换启动文件就会这样。尤其当原工程里用了定时器中断、串口中断且这些中断号在md.s向量表里不存在或索引错位时中断一旦触发就是HardFault。排查链路可以这样复现进入调试模式打开Peripherals - Core Peripherals - Exception / Vector Table查看当前向量表基地址和长度。看Call Stack窗口发现程序停在HardFault_Handler往上找不到main函数里的任何调用关系。在stm32f10x_it.c里打断点观察是哪个中断服务函数没有正常进入。确认工程里启动文件还是startup_stm32f10x_md.s换成hd.s后重新编译下载问题消失。判断启动文件是否生效的另一个方法编译后在Build Output窗口看链接信息正常时会出现Reset_Handler、NMI_Handler等符号来自startup_stm32f10x_hd.o。4.2 案例Flash Download Failed或下载算法报错换芯片后下载报错常见的有两种表现。一种是在Flash Download页面里还留着旧算法下载器识别到擦除地址超出了中密度算法支持的0x10000范围报类似“Erase Failed!”或者“Flash Download failed - Target DLL has been cancelled”的错误。解决方式是进Debug - Settings - Flash Download把Med-density算法删掉换成High-density Flash 256K同时把Programing Algorithm的Start Address设为0x08000000Size设为0x40000。另一种是算法选对了但勾选了“Erase Full Chip”每次烧录都全片擦除RCT6的256KB Flash擦除时间比C8T6长不少看起来就像卡死了。如果只是升级固件建议选“Erase Sectors”只擦除程序占用的扇区烧录速度快很多。4.3 案例SWD连接失败、No target connectRCT6的板子如果SWD连不上ST-Link先不要怀疑芯片坏了。按下面顺序排查确认供电正常VDD引脚有3.3VGND测量为0V。确认BOOT0跳线接到GND如果BOOT0悬空或者拉高芯片可能停在BootloaderSWD扫描会失败。确认BOOT1状态。RCT6的PB2就是BOOT1如果你在这个引脚上接了上拉电阻并且BOOT0也拉高芯片就会进入系统存储器启动用户Flash完全被跳过。检查SWDIO和SWCLK是否接反、是否接触良好。ST-Link的SWD接口在部分板子上需要单独供电。如果还是连接失败按住板子复位键在MDK里点Connect的同时松开复位利用目标芯片在复位瞬间的短暂时间窗口建立连接。对于Flash里跑飞代码导致SWD引脚被抢占的情况这个办法很有效。这三个案例前两个是换型操作本身引起的第三个更多是硬件连接问题。它们之间的共同点在于现象看起来都像芯片坏了实际上都是工程配置或启动条件不对。5. 从C8升到RCT6之后IAP和量产层面的再规划5.1 如果原来有BootloaderApp偏移要重新规划很多C8T6产品为了做OTA把Flash分成Bootloader和App两个区域Bootloader放0x08000000App放在0x08008000偏移32KB或者0x08010000偏移64KB)。C8T6总共才64KBApp区域非常紧张。换到RCT6后App空间猛增到200KB以上这个时候建议重新规划分区。比如Bootloader占用32KB那么App起始地址放在0x08008000不变但App最大可以从0x08008000一直用到0x0803FFFF灰常充足。如果原来为了省空间把App偏移设在0x08004000现在也可以考虑调整到0x08010000好处是Bootloader区域更大可以集成更丰富的固件升级协议。但要注意Bootloader的APP起始地址、App工程的IROM1起始地址、系统中断向量偏移量NVIC_SetVectorTable三者必须严格一致。改哪一处都要同步另外两处否则就会出现在Bootloader里能跳转但App跑起来中断全部失灵的问题。5.2 编译输出的四个数字该看哪个换型后编译Build Output窗口里会显示Program Size: Codexx RO-dataxx RW-dataxx ZI-dataxx很多人只看Code大小其实RW-data和ZI-data才是SRAM占用的关键。RW-data是初始化为非零值的全局变量上电时从Flash拷贝到SRAMZI-data是初始化为零的变量占SRAM但不占Flash。两者加起来就是静态SRAM占用。如果换到RCT6后ZI-data接近48KB说明RAM规划有问题。可以打开.map文件搜索“Maximum Stack Usage”确认堆栈深度再决定是否需要调整启动文件里的Stack_Size。hd.s默认栈大小一般是0x4001KB如果工程里用了大量局部数组或者RTOS任务栈建议改大比如0x10004KB这是RCT6换型后经常忽略的优化点。5.3 芯片差异之外的量产验证建议最后说点量产层面的建议。芯片型号变更之后即使代码完全兼容也不能直接拿原来的固件文件烧录到RCT6批量出货。第一要在量产烧录前做一次全片擦除验证。因为RCT6的Flash是256KB如果替换时用的烧录算法还是旧的64K高地址区域可能残留之前测试数据导致芯片运行时读到异常数据。第二确认固件版本号和芯片信息是否写入了固定地址。很多产品会在Flash里存一个版本结构体换型后最好在打包脚本里检查CRC值和芯片容量标记防止生产线上烧错固件。第三如果产品有读保护需求换型后的Option Byte配置流程要重新验证。C8T6和RCT6的读保护设置接口相同但不同批次、不同容量等级的Flash操作时序可能存在细微差异建议用RCT6的样片完整跑一遍加读保护、读回校验、解除保护的流程。我自己现在的做法是新建工程时就直接按RCT6来建立板级配置文件和宏定义目录需要出C8T6小体积版本时用条件编译切回去几分钟就能出一版固件。这样C8T6和RCT6在我的项目里基本就是一对随时互换的“孪生兄弟”了。这套迁移流程走了这么多次最省时间的核心就一句话启动文件、预处理宏、下载算法三处都对代码基本不用动。