简介:作为经典MCU系列中的代表型号之一,STM32F103C8T6是意法半导体基于ARM Cortex-M3内核的32位微控制器,最高主频72MHz,内置64KB闪存与20KB运行内存,并集成多种通信接口、模数转换器与定时器,广泛用于物联网、工业控制和智能家居等领域。内容面向嵌入式初学者及需要快速落地STM32项目的工程师,系统梳理了从内核架构、存储结构、外设配置到低功耗模式、开发工具选型、在线调试方法和封装引脚分布的完整知识链,同时附有最小系统客户资料便于对照实践。压缩包约50.46MB,其中的“STM32F103C8T6最小系统(客户资料)-1411”预计包含电路原理图、PCB布局参考与固件代码示例,能帮助读者理解最小系统的硬件设计要点,并结合Keil或一体化开发环境完成烧录与调试。已有2514人学习,适合零基础自学、课程设计或产品预研时参考,对缩短项目上手周期有明显帮助。同时内容支持离线查阅,方便随时对照学习。 STM32F103C8T6这颗芯片,说它是入门ARM的“国民级”单片机一点不夸张。你可能在淘宝上花十几块买过一块蓝色最小系统板,插上USB线就能点灯,也看过各种原理图、引脚图、工程模板满天飞。但资料越多越容易乱:有人卡在CubeMX配时钟树,有人被串口中断“只收一次”折磨半天,还有人想用HAL库模拟I2C读个磁编码器,一读出来数据全是跳变的。
这篇文章就围绕F103C8T6,把这些高频问题串起来聊一遍。内容既覆盖芯片本身的核心资源和最小系统板原理,也会讲到开发环境选型、若干实战项目(密码锁、HX711称重、MT6701编码器)、通信模块接入(ESP01S、A7670C 4G的MQTT),最后补充国产替代和固件加密踩坑经验。适合刚接触STM32的初学者,也适合正在做具体项目、想要一份避坑清单的开发者。
1. 先说清楚F103C8T6这颗芯片的底细
1.1 核心规格:这颗单片机到底有多少家底
很多人把F103C8T6当成“能跑操作系统的单片机”,其实它定位是主流级Cortex-M3内核的单片机,最高主频72MHz(我实际用的时候一般都配到64MHz或者72MHz),Flash 64KB,RAM 20KB。听起来不大,但点亮屏幕、跑个小型RTOS、做Modbus从站、处理几个传感器数据流都足够。
比较重要的外设资源我列一下:3个USART、2个SPI、2个I2C、1个CAN、1个USB 2.0全速设备控制器,还有2个12位ADC(总共10个通道)。定时器方面有4个16位定时器(TIM1/TIM2/TIM3/TIM4),其中TIM1是高级定时器,能做互补PWM输出,这也是为什么很多人拿它驱动无刷电机或者做数字电源。
跟个人电脑相反,这颗单片机资源是“紧巴巴里的富余”,对初学者来说反而友好——你不用花时间在一堆用不上的功能里做选择。比如做一款智能家居安防系统,一个USART接ESP01S连WiFi,一个USART接调试串口,一个I2C接OLED,几个GPIO接门磁和人体红外,资源刚好卡在舒适区。
1.2 引脚和最小系统板:为什么蓝色板子人手一块
F103C8T6是LQFP48封装,一共48个引脚,其中可用的GPIO是37个(PA0-PA15、PB0-PB15、PC13-PC15以及PD0/PD1,但PD0/PD1通常被外部晶振占用了,除非你用内部HSE否则不太建议用)。
很多人的第一块开发板是那种蓝色最小系统板。它的原理图并不复杂,核心就是几部分:8MHz外部晶振配合两个20pF电容给HSE提供时钟源;一个1117-3.3把USB的5V转成3.3V给芯片供电;复位引脚上拉10k电阻再加一个100nF电容做上电复位;BOOT0和BOOT1跳线用于选择启动模式;SWD下载口直接引出PA13(SWDIO)和PA14(SWCLK)。
这块板子最容易让人困惑的是:板载的那个USB口,只是供电和串口通信,芯片的USB引脚(PA11、PA12)并没有直接连到USB座上,中间隔了一颗CH340串口芯片。所以你想把F103C8T6当USB设备用(比如模拟成鼠标键盘),在蓝色板子上是做不到的,得自己飞线或者换带原生USB引脚的板子。
最小系统板还有两个常见变体,一个是带板载LED(通常接PC13),一个是带8MHz晶振和32.768kHz低速晶振的完整版本。蓝色板子默认没有32.768kHz晶振,这意味着RTC要走内部低速时钟(LSI),精度远不如外部晶振,做RTC时钟项目时要注意。比如网上很多人用F103做电子时钟、密码锁带时间戳,第一版就发现每天走时误差能到十几秒,根源就在这里。
2. 开发环境怎么选:寄存器、标准库还是HAL
2.1 三条开发路线的真实差异
F103C8T6的开发方式现在主要有三种:直接写寄存器、使用标准外设库(SPL)、使用STM32CubeMX生成HAL库工程。Arduino也算一个选项,但工程上用得不多,后面单独聊。
直接操作寄存器最接近芯片本质,比如你要把PA5拉高,就直接写GPIOA->BSRR = GPIO_PIN_5。好处是代码量最小、执行效率最高,坏处是配置一个串口要翻参考手册查十几个寄存器的位定义,调试周期长。标准外设库是ST官方早期的封装,命名还算直观,但自HAL库推出后基本停止维护了。现在做新项目,我更推荐HAL库——不是因为它性能好,而是因为CubeMX的可视化配置能帮你避掉大量低级错误,比如时钟树配错导致USART波特率不对。
从找工作角度说,很多公司存量项目还在用标准库,所以两条路线都值得会。但从2024年前后的新项目看,HAL基本是主流,新工程师入职后接手的也都是HAL工程,所以直接学HAL是性价比最高的路径。
2.2 用STM32CubeMX创建一个LED工程:从零到点亮
我用CubeMX建LED工程不下几十次,每次碰到新手卡住,无非就是那几步。以F103C8T6为例,完整流程是这样:
第一步,在CubeMX的芯片选型界面搜“STM32F103C8”,双击选中。第二步,在Pinout视图里先把SYS->Debug Serial Wire开启,这一项不做的话,你第一次烧录程序后第二次就下载不进去了,SWD引脚会被初始化为普通GPIO导致调试器连不上。第三步,配RCC->HSE为Crystal/Ceramic Resonator,然后打开Clock Configuration视图,把HCLK改成72MHz,系统会自动反推PLL的配置,SYSCLK那一栏变成72就对了。
第四步,如果LED在PC13,那就在Pinout里左键点PC13选GPIO_Output。如果想用按键控制LED,再把某个引脚设为GPIO_Input。如果你用的是PA5(很多板子上LED也在PA5),就选PA5。第五步,Project Manager里Toolchain选择MDK-ARM或STM32CubeIDE,生成工程,然后在main函数while(1)里写HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13); HAL_Delay(500);编译下载,LED就闪起来了。
这段流程里最容易出问题的是时钟树。如果HCLK不是72MHz而是8MHz,说明PLL没有打开,生成代码后实测点灯没问题,但串口波特率、定时器定时时长全部会偏。所以工程生成后先看一眼SystemClock_Config函数里的HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2)是不是FLASH_LATENCY_2,Flash等待周期不对,72MHz下跑着跑着就会死机。
3. 从点灯到做产品:四个值得复盘的实战项目
3.1 密码锁项目:状态机和掉电存储
网上关于“STM32F103C8T6密码锁”的教程很多,但大多数逻辑都有隐患。我之前做过一个带矩阵键盘(4x4)和OLED显示的门禁密码锁,核心逻辑是状态机:等待输入->校验密码->开锁/报错->重新上锁。密码输入过程中要做按键消抖,一般用10ms延时消抖或者定时器扫描去抖,之后再做按键值映射。
密码锁最容易翻车的地方有两个。第一个是密码怎么存:很多人直接把密码硬编码在Flash里,这样改密码只能重新烧录。正确做法是把密码放在EEPROM里,F103C8T6没有内部EEPROM,所以项目里通常会外挂AT24C02(I2C)。HAL库的I2C操作APi是一次读一页,但要注意AT24C02的页写只有8字节,跨页写要拆分,不然会丢数据。
第二个容易翻车的是“掉电保持开锁状态”这个需求。有人靠检测VBUS电压掉电后立即保存状态到Flash,但Flash擦写次数有限,频繁开关锁会损耗Flash寿命。我当时用的方案是EEPROM里存一个标志位加时间戳,默认上电锁定,除非检测到合法开锁动作并且断电发生在开锁后5秒内,否则上电不恢复开锁状态。这样逻辑简单,也不会因为频繁写EEPROM把I2C总线和存储芯片搞崩溃。
3.2 HX711称重模块:模拟时序读取24位ADC
HX711是电子秤常用的一款24位高精度ADC芯片,通过DOUT和PD_SCK两根线跟单片机通信,时序上是典型的SPI-like,但用GPIO模拟完全没问题。F103C8T6的HAL库虽然有硬件SPI,但HX711的时序要求“当PD_SCK为低时DOUT输出数据”,并且24个时钟脉冲后自动切换通道和增益,所以直接用两个GPIO配合延时去读反而更稳。
我调试HX711时遇到的最大坑,是模块的供电问题。HX711的模拟电源AVDD通常由稳压芯片提供5V电压(给称重传感器激励),但它的数字电源VCC接3.3V,两套电源不能直接混用。如果直接用3.3V给传感器供电,称重灵敏度会明显下降。另外传感器输出信号是差分的小信号,线不能太长,最好用屏蔽线,否则读数会漂得很厉害。
软件上最影响体验的是“标定”这一步。正确流程是:上电后空载状态下读100次取平均得到零点偏移,然后把已知重量(比如100g砝码)放上去读100次取平均,再得到满量程偏移。这样重量 = (当前读数 - 零点) / (砝码读数 - 零点) * 砝码重量。代码里建议用float,并且把校准系数存进EEPROM,不然每次重新上电都得重新标一次。
3.3 MT6701磁编码器:I2C读取、滤波与零点校准
MT6701是一个14bit分辨率的磁编码器,支持I2C接口,用于电机角度检测。我曾在无刷电机FOC项目里用它做转子角度反馈。用HAL库模拟I2C读取MT6701,注意它支持的I2C地址是0x06(7位地址),寄存器地址0x00存放角度值的高8位,0x01存放低6位,组成一个14位原始角度数据。
有人读完数据发现角度值在0到16383之间跳变,觉得是I2C时序问题。其实更常见的原因是零点没有校准:MT6701的机械零点跟磁铁位置有关,上电第一次读到的是绝对角度,但这个绝对角度并不等于电机电角度的零度,所以必须先让电机跑一遍“找零”程序,把当前角度设定为电气零度。之后每次读数减去零点的offset,再对16384取模,角度就连续了。
滤波也是这个项目里的关键一环。MT6701本身噪声并不大,但电机PWM产生的电磁干扰会让读数跳几个LSB。我实测用简单的一阶低通滤波(angle_filtered = (1-k)*angle_filtered + k*angle_raw,k取0.1到0.2)效果就不错。注意不能在读角度的回调里做浮点运算,否则会影响控制环的刷新率。更好的做法是开一个定时器中断,中断频率1kHz读一次角度,主循环里跑滤波和控制算法。
3.4 智能家居安防系统:多传感器和低功耗取舍
用F103C8T6做智能家居安防系统,很多人会把各种传感器一股脑接上去:人体红外、门磁、烟雾、温湿度、OLED、蜂鸣器、WiFi模块,全部常年供电运行。结果功耗高、发热大、电池供电没两天就没电。
其实这种系统更应该做“事件驱动”:平时单片机进入睡眠模式(WFI或STOP模式),靠外部中断唤醒。比如人体红外传感器输出高电平引发EXTI中断,从睡眠中唤醒后再去执行拍照、发MQTT消息等重活。F103C8T6没有独立的低功耗模式配置寄存器,但要区分Sleep模式和Stop模式:Sleep模式功耗约mA级,Stop模式约uA级,但Stop模式下外设时钟会停掉,唤醒后需要重新初始化。我自己做安防面板的时候,是让STM32常驻Stop模式,定时器每隔500ms唤醒一次去轮询传感器状态,这样整机静态电流能压到几百微安这个量级。
4. 通信模块接入的避坑指南
4.1 ESP01S连接WiFi:别让供电坑了你
ESP01S这颗WiFi模块很便宜,出厂一般是AT固件,用串口就能控制它连接WiFi、建立TCP连接。很多人第一次把ESP01S直接接到F103C8T6的3.3V上,发现透传数据全是乱码或者模块不断重启,九成都是供电不足导致的。ESP01S在发射瞬间电流能到300mA以上,而板载1117-3.3的输出能力有限,一旦射频发射掉电就会复位。正确做法是给ESP01S单独准备一个电流余量足够的3.3V电源,或者用AMS1117单独供一路,同时并一个470uF电解电容。
软件上,用AT指令连接WiFi的顺序很重要。上电后先发“AT”测试响应,然后“AT+CWMODE=1”设置Station模式,“AT+CWJAP=“SSID”,“PASSWORD””连接热点,最后“AT+CIPSTART=”TCP“,”服务器IP“,端口”建立连接。注意不要一条指令发完马上发下一条,ESP01S内部处理AT指令需要时间,最好等串口收到OK再发下一条。我当时用状态机加超时重发来处理,比固定延时可靠得多。
4.2 A7670C 4G模块MQTT通信:从AT指令到数据上云
A7670C是一款4G Cat.1模块,跟F103C8T6的对接方式跟ESP01S类似,也是用AT指令驱动串口通信,但坑更多。第一个坑是模块启动时间很长,上电后需要等待几秒到十几秒,期间不接受AT指令,必须等URC上报“RDY”或者“+CPIN: READY”。第二个坑是MQTT指令跟普通AT指令格式不同,以连接到OneNET平台为例,需要依次发送:
AT+QMTOPEN=0, "broker地址", 1883 AT+QMTCFG="recv/mode",0,0,1 AT+QMTCONN=0, "设备id", "username", "password" AT+MQTTSUB=0, "主题名", 1 AT+MQTTPUB=0, "主题名", 1, "消息内容"这里最容易踩的坑是:订阅和发布的主题一定要跟OneNET平台上创建的数据流保持一致,而且消息体必须是平台要求的JSON格式,比如{"temp":25.5}。如果你直接发temp=25.5,平台会返回错误或者数据不显示。另外AT指令里的逗号和双引号都是半角,如果用了中文输入法打出来,模块直接不识别。我曾因为字符串里带了一个全角空格,排查了整整一个下午。
建议把MQTT连接过程封装成状态机:上电->等待模块就绪->配置网络->连接MQTT->定时上报。不要在主循环里阻塞等待模块响应,F103C8T6的USART中断配合环形缓冲区来处理AT响应最稳妥。具体实现时,可以开一个256字节的接收缓冲,裸机轮询解析“+MQTT”前缀的事件即可。
4.3 串口中断只收一次?多半是这里的问题
F103C8T6用HAL库配置USART接收中断,最经典的坑就是“串口中断只收一次,之后就再也收不到”。原因基本就三个:一是HAL库默认开启的是单字节接收,接收到一个字节后中断被关闭,需要你在中断回调里重新调用HAL_UART_Receive_IT再次开启接收。二是中断回调函数里做了耗时操作,比如printf、延时,导致第二个字节来的时候内核还在忙,标志位覆盖。三是没有清除串口的ORE(OverRun Error)标志,数据来了直接丢。
解决思路也很直接:中断回调里只做一件事,把接收到的字节放进一个环形缓冲区,然后立即重新HAL_UART_Receive_IT。主循环或定时器中断里再去解析缓冲区内容。这样串口就不会“只收一次”了。如果用了DMA接收,则要关注IDLE中断和乒乓buffer的问题,情况会更复杂一些,但对F103C8T6这种RAM有限的芯片,环形缓冲区方案已经足够。
5. 选型替代与固件加密的实用经验
5.1 国产替代方案:能用,但要关注细节
F103C8T6因为性价比高,市面上有大量国产兼容芯片,比如GD32F103C8T6、APM32F103C8T6等,引脚和大部分寄存器都兼容,很多工程可以直接把芯片换个丝印重新编译烧录。但实际项目里不能把它当完全对等的东西用,我第一次用GD32替换的时候,发现USART的波特率误差跟STM32略有不同,跑高速通信(比如460800以上)的时候偶发错帧。
另外要留意Flash擦写差异。GD32的Flash分扇区大小和STM32不完全一样,如果用IAP升级(在应用内把新固件写到Flash再跳转),擦除地址可能会擦掉别的扇区内容。我在做OTA升级时就被这个坑过,最后在工程里加了“根据芯片ID号区分是STM32还是GD32,用不同的扇区映射表”的逻辑。所以在项目一开始就考虑国产替代的话,最好先确认量产固件跑在哪种芯片上,避免后续兼容性排查。
5.2 固件加密与读保护:不是简单的“锁定”
很多人以为固件加密就是设个密码不让别人下载程序,其实STM32的读保护(RDP)有三档:Level 0是完全开放,Level 1是禁止调试器读取Flash,Level 2是永久禁止读取且不可回退。用STM32CubeProgrammer可以很方便地把RDP设置到Level 1,设置后普通下载器只能执行全片擦除才能重新下载,这就防止了别人直接读Flash里代码。
但要注意几点。第一,开启Level 1后如果误操作,只能用“Full chip erase”把整片Flash擦掉,所有代码都没了,所以在量产最后一步再设读保护。第二,如果芯片里代码使用了唯一ID(UID)绑定功能,开启读保护后不影响UID读取。第三,国产替代芯片对RDP的等级定义可能跟原厂不完全一样,GD32的Level 1能不能支持“用串口ISP全片擦除”要提前验证。加密的最终目的不是让数据完全不可见,而是让对方获取代码的成本超过买一颗正版芯片的成本,这样你的代码逻辑才算真的受保护。
写在最后
F103C8T6这颗芯片我前前后后用了快十年,从当年写寄存器点亮LED,到后来用CubeMX生成整套HAL工程,再到国产替代、OTA升级,踩过的坑远比这篇写在纸面上的多。我个人的体会是:学单片机不能只满足于“点亮一个灯”,真正增长经验的地方,是在你第一次被串口丢数据折磨、第一次发现引脚冲突、第一次从Datasheet里找到答案的那些瞬间。芯片只是工具,而资料的价值,在于帮你把时间花在对的问题上。下次再遇到项目,别急着搜别人的代码,先打开参考手册和原理图,自己推一遍,你会发现这条路虽然慢,但走得最稳。
本文还有配套的精品资源,点击获取