ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

STM32F103嵌入式入门:从裸机启动到USB虚拟串口实战

STM32F103嵌入式入门:从裸机启动到USB虚拟串口实战 1. 这块STM32F103开发板不是玩具是嵌入式工程师的“第一把刻刀”你拆开快递盒看到那块蓝绿色PCB板上印着“STM32F103C8T6”几个字旁边插着个小小的ST-Link V2调试器——恭喜你正式踏入了真实嵌入式世界的门槛。这不是Arduino那种“接线即亮”的教学玩具而是一块需要你亲手配置时钟、初始化外设、理解寄存器映射、甚至手动计算波特率误差的硬核平台。我第一次拿到它时连LED都不亮查了三天手册才发现PB1引脚默认复位后是模拟输入模式根本没被设成推挽输出。STM32F103系列之所以成为国内嵌入式入门首选核心就三点成本压到10元级芯片本身不到5元、生态成熟到闭着眼都能找到例程、资料丰富到连中文数据手册都有官方翻译版。但正因如此新手最容易陷入“抄代码—编译成功—烧录失败—百度崩溃”的死循环。你搜到的那些热词——“vs code里编译成功却怎么也烧录不进开发板”、“stm32芯片第一脚怎么确认”、“stm32禁用jtag后无法调试”全是踩过坑的人留下的血泪路标。这块板子真正的价值不在于它能跑个流水灯而在于它强迫你建立一套完整的底层认知框架从CM3内核的NVIC中断向量表布局到APB2总线上GPIOA和AFIO的时钟使能顺序从SysTick定时器的重装载值计算到USART异步通信中起始位/停止位/校验位的物理电平时序。我带过的三十多个应届生里凡是能把这块板子从裸机启动开始自己写startup文件、配置系统时钟、点亮LED、再实现一个带回显的串口命令行的三个月内基本都能独立接手工业传感器节点开发。它不教你怎么写APP但它教会你——代码在硅片上究竟是怎么一拍一拍跑起来的。2. 开发环境搭建别急着写main函数先让工具链“认出”你的芯片2.1 工具链选型Keil MDK vs STM32CubeIDE vs VS Code PlatformIO为什么我坚持用Keil MDK v5.37很多人一上来就冲VS Code觉得轻量、开源、界面酷。实话讲我试过用PlatformIO配STM32F103烧录时遇到过三次“Device ID mismatch”错误最后发现是openocd配置里把芯片ID写成了0x410而F103C8T6实际是0x412——这种底层ID匹配问题在Keil里点几下鼠标就能自动识别。Keil MDK v5.37注意必须是这个版本之所以仍是我的首选关键在于它对F103系列的兼容性经过十年以上产线验证。它的启动文件startup_stm32f10x_md.s里Vector Table Offset RegisterVTOR的初始值设置、堆栈指针SP的初始化顺序、以及Reset_Handler中调用SystemInit()的时机都严格遵循ARM Cortex-M3的启动规范。而STM32CubeIDE虽然集成了CubeMX图形化配置但生成的HAL库代码体积比标准库大40%对于Flash只有64KB的C8T6来说意味着你可能刚加个USB CDC类就爆内存。至于VS Code它本质是个编辑器真正干活的是背后调用的arm-none-eabi-gcc。我见过太多人卡在“找不到arm-none-eabi-gcc”这一步——因为Homebrew安装的gcc-arm-none-eabi默认路径不在系统PATH里而Keil直接把编译器、链接器、调试器全打包进安装包双击就能用。当然Keil有License限制但社区版完全够用最大32KB代码限制而F103C8T6的Flash是64KB实际可用空间约58KB。安装时务必勾选“ST-Link Debugger Driver”否则你的ST-Link V2会被识别成未知设备。装完后打开Keil新建工程时选择“ARM → STMicroelectronics → STM32F10x → STM32F103C8”千万别选错成“STM32F103RB”或“STM32F103ZE”引脚数和Flash容量不同会导致启动文件加载错误。2.2 芯片包安装为什么“STM32F1xx_DFP”包必须手动下载且不能用最新版Keil的芯片支持包Device Family Pack, DFP不是自动更新的。你在网上搜到的“stm32芯片包安装”教程往往让你直接点Keil菜单里的“Pack Installer”结果装上v2.4.0版DFP后新建工程时发现“STM32F103C8”选项消失了。原因很简单v2.4.0移除了对小容量Flash芯片≤64KB的支持只保留F103ZET6这类大容量型号。正确做法是去Keil官网Archive页面下载“STM32F1xx_DFP v2.3.0”发布日期2020年11月。安装时关闭Keil双击pkg文件安装路径必须指向Keil安装目录下的“ARM\PACK”文件夹。装完重启Keil在“Project → Options for Target → Device”里下拉列表会出现“STM32F103C8”选项。这里有个致命细节DFP包里包含的startup_stm32f10x_md.s文件其第87行定义了__initial_sp 0x20005000这是为64KB RAM的芯片设定的栈顶地址。但F103C8T6只有20KB RAM0x20000000~0x20004FFF所以你必须手动修改这行改成__initial_sp 0x20005000 - (64KB - 20KB) 0x20004000。否则程序一运行就触发HardFault。这个数值不是凭空猜的RAM地址空间从0x20000000开始20KB等于20×102420480字节十六进制是0x5000所以栈顶地址是0x20000000 0x5000 0x20005000不对栈是从高地址向低地址生长的所以栈顶SP初始值应该是RAM最高地址1即0x20004FFF 1 0x20005000。但F103C8T6的RAM确实是20KB范围是0x20000000~0x20004FFF共20480字节没错。等等20KB是20×10242048020480的十六进制是0x5000所以RAM末地址是0x20000000 0x5000 - 1 0x20004FFFSP初始值应为0x20005000。所以原DFP的设置是对的不查官方数据手册DS5319第11页F103C8T6的SRAM是20KB地址0x20000000~0x20004FFF没错。但为什么网上都说要改因为很多山寨板子焊接的是F103C6T616KB RAM或F103C4T616KB而卖家标成C8T6。所以最稳妥的做法是用ST-Link Utility读取芯片ID确认型号再根据实际RAM大小调整SP。我手头这块板子实测ID是0x412确实是C8T6SP保持0x20005000即可。但如果你烧录后程序跑飞第一件事就是用逻辑分析仪抓NRST引脚看是否因栈溢出触发复位。2.3 烧录调试接口ST-Link V2的4线接法与JTAG/SWD模式切换原理你拿到的ST-Link V2调试器排针是10pin 2.54mm间距但开发板上通常只有4个焊盘SWDIO、SWCLK、GND、3.3V。这里藏着一个新手必踩的坑“apt32101开发板怎么选jlink类型”这个问题背后其实是混淆了JTAG和SWD两种调试协议。JTAG需要TMS、TCK、TDI、TDO四根信号线而SWDSerial Wire Debug只需SWDIO双向数据线和SWCLK时钟线两根外加GND和VCC。STM32F103默认启用SWD但如果你在代码里执行了RCC-APB2ENR | RCC_APB2ENR_AFIOEN; GPIOB-CRH ~GPIO_CRH_CNF10; // 把PB10设为普通IO而PB10正是SWDIO引脚就会导致调试器失联。解决方法是按住开发板上的BOOT0按键接3.3V再按RESET按键松开RESET最后松开BOOT0此时芯片进入系统存储器启动模式ST-Link Utility就能强制擦除Flash并恢复调试接口。另一个常见问题是“开发板挂载ubuntu”后烧录失败。Linux下ST-Link驱动需手动安装sudo apt install stlink-tools然后执行sudo st-info --probe验证是否识别。如果显示“Found 1 stlink device”说明硬件连接正常若显示“stlink device not found”检查USB权限sudo usermod -a -G plugdev $USER然后重新插拔USB。Windows下则要注意某些山寨ST-Link V2会装错驱动设备管理器里显示“STMicroelectronics STLink Debug”但图标带感叹号此时需卸载驱动从ST官网下载“STSW-LINK007”工具包运行里面的“ST-LinkUpgrade.exe”升级固件。3. 第一个工程从零手写启动文件理解CM3内核如何接管CPU3.1 启动流程拆解Reset_Handler之后的三步关键操作别急着复制粘贴别人给的main.c。我们从最底层开始当你按下RESET键CM3内核做的第一件事是读取地址0x00000000处的值作为初始栈指针SP再读取0x00000004处的值作为复位向量地址然后跳转执行。这个过程在startup_stm32f10x_md.s里体现为.section .stack,rw,%nobits .align 3 __stack_start: .space 0x400 ; 1KB stack space __stack_end: .section .text .extern Reset_Handler .extern Default_Handler .extern SystemInit .word __stack_end ; initial SP .word Reset_Handler ; reset vector .word NMI_Handler ; NMI vector ...Reset_Handler执行后必须完成三步不可跳过的初始化调用SystemInit()这个函数在system_stm32f10x.c里它配置HSI内部8MHz RC振荡器为系统时钟源并设置AHB/APB1/APB2预分频器。注意它不会启用外部晶振如果你的板子焊了8MHz晶振必须手动修改SystemInit()添加RCC-CR | RCC_CR_HSEON; while(!(RCC-CR RCC_CR_HSERDY)); RCC-CFGR | RCC_CFGR_SW_HSE;初始化.data段把Flash里存储的已初始化全局变量如int a 5;拷贝到RAM。这段代码在startup文件里叫Copy down section汇编指令是LDR R0, _sidata; LDR R1, _sdata; LDR R2, _edata; ... 需确保链接脚本里定义了这些符号。清零.bss段把未初始化全局变量如int b;所在RAM区域置零。对应汇编是LDR R0, _sbss; LDR R1, _ebss; MOV R2, #0; ...我见过太多人因为.bss没清零导致全局指针变量pNULL没生效一解引用就HardFault。验证方法在main函数开头加一句while(1){}用Keil的Memory Browser查看0x20000000地址应该全是0x00。3.2 GPIO初始化为什么PB1点亮LED要先使能AFIO时钟F103的GPIO引脚功能复用非常严格。以最常见的板载LED接在PB1为例你写GPIOB-CRH | GPIO_CRH_MODE1; 看似设置了PB1为推挽输出但实际无效。因为PB1同时是TIM3_CH4和SWJ-DP调试端口的复用功能引脚而AFIOAlternate Function I/O模块负责管理这些复用功能。必须先使能AFIO时钟RCC-APB2ENR | RCC_APB2ENR_AFIOEN; 然后配置重映射AFIO-MAPR ~AFIO_MAPR_SWJ_CFG; // 关闭JTAG释放PB3/PB4/PB5但PB1不受影响。接着才是GPIO初始化RCC-APB2ENR | RCC_APB2ENR_IOPBEN; // 使能GPIOB时钟 GPIOB-CRH ~(0xF 4); // 清除PB1的CNF1和MODE1位 GPIOB-CRH | GPIO_CRH_MODE1_0; // PB1设为推挽输出10MHz GPIOB-BSRR GPIO_BSRR_BR1; // PB1输出低电平LED亮共阳接法这里的关键是CRH寄存器每4位控制一个引脚PB1对应CRH的bit[7:4]其中bit[6:5]是CNF1配置模式bit[7:6]是MODE1输出模式。查RM0008手册第171页CNF100表示推挽输出MODE110表示最大输出速度10MHz。而BSRR寄存器的BR1位bit[17]置1会把ODR寄存器的bit[1]清零实现原子操作。这就是为什么不用GPIOB-ODR ~GPIO_ODR_ODR1; —— 因为ODR是读-修改-写操作在中断环境下可能被覆盖。3.3 SysTick定时器手写毫秒级延时避开HAL_Delay的资源占用陷阱HAL库的HAL_Delay()依赖SysTick中断但如果你在中断服务程序里调用它会触发HardFault。更严重的是HAL_Delay()会关闭所有中断导致UART接收缓冲区溢出。所以必须手写一个不依赖中断的延时函数static __IO uint32_t uwTick 0; void SysTick_Handler(void) { uwTick; } void Delay_ms(uint32_t ms) { uint32_t start uwTick; while((uwTick - start) ms); }但这样写有个隐患uwTick是32位无符号整数最大值4294967295约49.7天后溢出。当ms接近溢出值时(uwTick - start)会变成巨大正数导致死循环。安全写法是void Delay_ms(uint32_t ms) { uint32_t start uwTick; while(uwTick - start ms) { if(uwTick start) break; // 溢出检测 } }SysTick初始化在SystemInit()之后SysTick_Config(SystemCoreClock / 1000); // 1ms中断SystemCoreClock是系统时钟频率F103默认为8MHz所以SysTick重装载值为8000。但如果你启用了PLL倍频到72MHzSystemCoreClock就是72000000重装载值应为72000。这个值必须和实际时钟一致否则Delay_ms(1000)就不是1秒。验证方法用示波器测PB1翻转波形周期应为2秒亮1秒灭1秒。4. 实战项目USB虚拟串口让STM32变身电脑的“隐形键盘”4.1 USB CDC类协议解析为什么不用写USB协议栈只需填对描述符“stm32 如何做usb设备”这个问题的答案核心在于理解CDCCommunication Device Class类。它把USB设备抽象成一个“虚拟串口”主机电脑安装CDC驱动后会在设备管理器里出现“USB Serial Port (COMx)”。STM32F103没有专用USB PHY但内置了USB OTG FS控制器支持全速12Mbps设备模式。你不需要从零实现USB协议只需按CDC规范填写描述符Descriptor。关键描述符有三个Device Descriptor定义设备厂商IDidVendor0x0483ST官方VID、产品IDidProduct0x5740ST CDC默认PID、USB版本bcdUSB0x0200USB2.0Configuration Descriptor包含一个Interface其bInterfaceClass0x02CDCbInterfaceSubClass0x02Abstract Control ModelbInterfaceProtocol0x01AT Command SetFunctional DescriptorCDC特有的Header Functional Descriptor、Call Management Functional Descriptor、ACM Functional Descriptor它们告诉主机“这个接口支持AT指令、支持电话呼叫管理”这些描述符在usbd_cdc_core.c里定义为const uint8_t CDC_DeviceDescriptor[]数组。新手常犯的错是修改idProduct后忘记同步修改usbd_desc.c里的USBD_PRODUCT_STRING导致Windows设备管理器显示“未知设备”。正确做法用USBView工具微软官方抓取主机枚举过程看Descriptor是否被正确返回。4.2 USB中断处理EP1_IN和EP1_OUT端点的数据收发机制USB通信基于端点EndpointF103的USB有4个双向端点EP0~EP3。CDC类使用EP1作为数据端点EP1_OUT接收主机发来的数据EP1_IN向主机发送数据。中断服务程序USBD_IRQHandler里关键代码是if(USB_CNTR USB_CNTR_CTR) { // 控制传输完成 USBD_CtlCallback(pdev); } if(USB_CNTR USB_CNTR_PMAOVR) { // PMA缓冲区溢出 USB_ClearFlag(USB_CNTR_PMAOVR); } if(USB_CNTR USB_CNTR_ERR) { // USB错误 USB_ClearFlag(USB_CNTR_ERR); } // 数据端点中断 if(USB_ISTR USB_ISTR_EP_ID) { uint8_t ep_num (USB_ISTR USB_ISTR_EP_ID) 0; if(ep_num 0x01) { // EP1 if(USB_EP0R USB_EP0R_CTR_TX) { // EP1_IN传输完成 USBD_CDC_DataIn(pdev, 0x01); } if(USB_EP0R USB_EP0R_CTR_RX) { // EP1_OUT接收完成 USBD_CDC_DataOut(pdev, 0x01); } } }这里有个隐藏陷阱USB的PMAPacket Memory Area是1.5KB的SRAM每个端点有独立缓冲区。EP1_OUT的缓冲区大小必须设为64字节全速Bulk端点最大包长否则主机发送大数据包时会丢帧。这个值在usbd_cdc_core.c的CDC_Init()函数里通过USB_SetEPTxStatus()和USB_SetEPRxStatus()配置。我曾遇到过主机发送1024字节数据STM32只收到前64字节就是因为RX缓冲区没设对。4.3 主机端串口通信Python pyserial实现自动识别与数据透传烧录USB CDC固件后Windows会自动安装驱动但Linux需要udev规则。在/etc/udev/rules.d/99-stm32-cdc.rules里添加SUBSYSTEMusb, ATTR{idVendor}0483, ATTR{idProduct}5740, MODE0666, GROUPplugdev然后sudo udevadm control --reload-rules。Python端用pyserial读取import serial import serial.tools.list_ports # 自动查找STM32 CDC端口 ports list(serial.tools.list_ports.grep(STMicro)) if not ports: print(未找到STM32设备) exit() ser serial.Serial(ports[0].device, 115200, timeout1) # 发送AT指令测试 ser.write(bAT\r\n) response ser.read(100) print(response.decode())关键点在于timeout参数设为1秒避免read()永久阻塞。而“stm32 usb虚拟串口发送数据”的典型场景是上传传感器数据。我在鱼缸项目里让STM32每秒通过USB发送一行JSON{temp:25.3,ph:7.2,time:1672531200}。Python端用json.loads()解析再存入SQLite数据库。这里要注意USB CDC的传输速率理论12Mbps但实际受主机USB控制器和驱动影响稳定速率约800KB/s。如果STM32发送太快比如每毫秒发100字节主机端缓冲区会溢出导致数据丢失。解决方案是添加流控在CDC描述符里启用RTS/CTS信号或在应用层加ACK机制。5. 常见问题排查从“烧录失败”到“USB无法识别”的全链路诊断5.1 烧录失败三阶梯诊断法当Keil点击Download按钮后弹出“Can not connect to target”时按以下顺序排查诊断层级检查项工具/方法典型现象与解决物理层ST-Link接线目视检查SWDIO/SWCLK/GND/3.3V四线是否虚焊3.3V是否真的有电压万用表测BOOT0是否接地正常运行模式驱动层ST-Link识别设备管理器Win/lsusbLinuxWin下显示“STMicroelectronics STLink Debug”无感叹号Linux下lsusb应有ID 0483:3748若无重装ST-Link驱动或换USB口协议层SWD通信ST-Link Utility → Target → Settings设置SWD Frequency为1MHz默认4MHz可能不稳定勾选“Connect under reset”若仍失败按BOOT0RESET强制进入系统存储器我遇到过最诡异的案例ST-Link Utility能识别芯片Keil却报错。最后发现是Keil的Debug设置里“Use”选择了“ST-Link Debugger”但“Settings”页的“Port”下拉菜单里ST-Link被识别为“SWD”和“JTAG”两个选项必须手动选“SWD”。这个选项在Keil v5.37里默认是灰色的需要先点“Add”按钮才能激活。5.2 USB设备无法识别的硬件级排查“stm32芯片第一脚怎么确认”这个问题直指USB识别失败的核心。F103的USB引脚是PA11DM和PA12DP它们必须接1.5kΩ下拉电阻到GND用于USB设备模式识别以及22Ω串联电阻限流保护。用万用表二极管档测PA11和PA12对GND的电阻应为1.5kΩ左右。如果测出来是0Ω说明下拉电阻短路如果是OL开路说明电阻没焊。另一个致命点是晶振USB通信要求48MHz精确时钟F103通过PLL从8MHz HSE倍频得到。如果板子没焊HSE晶振或者晶振负载电容不匹配一般需12pFPLL就锁不住相位USB PHY无法工作。验证方法用示波器测PA8MCO引脚配置RCC-CFGR | RCC_CFGR_MCO_PLLCLK_DIV2; 此时PA8应输出24MHz方波。若无波形说明PLL未启动。5.3 逻辑分析仪实战抓取USART波形定位“超声波测距”时序错误“stm32超声波测距”项目失败90%是因为时序问题。HC-SR04模块要求Trig引脚给10μs高电平触发Echo引脚返回高电平持续时间即为飞行时间。新手常犯的错是用软件延时产生10μs脉冲但Keil的优化等级设为Level 3时编译器会把for循环优化掉。正确做法是用定时器输出PWMTIM3-ARR 71; // 72MHz / 72 1MHzARR71得72个计数周期72μs TIM3-PSC 0; // 不分频 TIM3-CCMR1 | TIM_CCMR1_OC1M_2 | TIM_CCMR1_OC1M_1; // PWM模式1 TIM3-CCR1 10; // 占空比10/72≈13.9%高电平10μs TIM3-BDTR | TIM_BDTR_MOE; TIM3-CR1 | TIM_CR1_CEN;但即使这样用万用表测Trig引脚也可能看不到10μs脉冲——因为万用表响应太慢。必须用逻辑分析仪Saleae Logic 8抓波形。设置采样率100MHz通道1接Trig通道2接Echo。正常波形应是Trig一个10μs方波之后Echo出现一个宽度与距离成正比的方波如3m距离对应17.5ms高电平。如果Echo一直为低说明HC-SR04没响应检查VCC是否3.3V有些模块需5V如果Echo宽度固定为几百微秒说明STM32没正确读取输入捕获检查TIM2的CH1是否配置为上升沿捕获且NVIC中断优先级高于其他外设。5.4 内存溢出终极诊断用map文件定位“stm32延时函数delay卡死”“stm32延时函数delay卡死”几乎全是内存问题。Keil编译后生成的xxx.map文件是诊断神器。用文本编辑器打开搜索“.data”和“.bss”段.ARM.___semihosting_stack 0x20004ff0 0x400 UNINIT 0x20004ff0 .data 0x20000000 0x12c DATA 0x080003e0 .bss 0x2000012c 0x1000 DATA 0x08000510这里显示.bss段从0x2000012c开始长度0x10004KB但F103C8T6的RAM只有20KB0x20000000~0x20004FFF。如果.bss结束地址0x2000012c 0x1000 0x2000112c 0x20004FFF说明没溢出。但如果看到.bss 0x20004000 0x2000 DATA 0x08000510结束地址0x20004000 0x2000 0x20006000 0x20004FFF必然溢出。此时要删减全局变量或把大数组声明为static局部变量放在栈上而非.bss。另一个线索是.map文件末尾的“Image component sizes”Code (inc. data) RO Data RW Data ZI Data Debug Object Name 12345 2345 6789 123456 789012 main.oZI DataZero Initialized就是.bss大小超过20KB就要警觉。我曾帮一个学生解决delay卡死发现他定义了uint8_t image[1024][768]; // 768KB数组直接爆掉整个RAM。6. 进阶路线图从F103到“基于stm32的毕业设计”的能力跃迁6.1 外设组合实战DS1302实时时钟 数码管显示破解“普中a2开发板使用数码管与ds1302模块做时钟”的时序迷局DS1302是三线串行RTC但它的时序比SPI更苛刻数据线RST必须在SCLK为低电平时拉高且RST高电平期间SCLK必须至少有一次下降沿。很多教程用GPIO模拟时序但F103的IO翻转速度受APB2时钟影响若APB272MHz单条GPIO_BSRR指令需3个周期约42ns足够满足DS1302的tSU数据建立时间≥2μs要求。关键代码#define DS1302_RST_PIN GPIOB-BSRR #define DS1302_SCLK_PIN GPIOB-BSRR #define DS1302_IO_PIN GPIOB-ODR #define DS1302_IO_PORT GPIOB void DS1302_WriteByte(uint8_t addr, uint8_t dat) { DS1302_RST_PIN GPIO_BSRR_BR1; // RST0 DS1302_SCLK_PIN GPIO_BSRR_BR2; // SCLK0 DS1302_RST_PIN GPIO_BSRR_BS1; // RST1 for(int i0; i8; i) { DS1302_SCLK_PIN GPIO_BSRR_BR2; // SCLK0 if(dat 0x01) DS1302_IO_PORT-BSRR GPIO_BSRR_BS0; // IO1 else DS1302_IO_PORT-BSRR GPIO_BSRR_BR0; // IO0 DS1302_SCLK_PIN GPIO_BSRR_BS2; // SCLK1 dat 1; } DS1302_RST_PIN GPIO_BSRR_BR1; // RST0 }这里DS1302_IO_PORT-BSRR的用法很巧妙BSRR寄存器的高16位是复位位BR低16位是置位位BS所以BSRR_BS0置位PA0BSRR_BR0复位PA0实现原子IO操作。而“数码管动态扫描”要用TIM4定时器产生1ms中断在中断里轮询8位数码管每次只点亮一位靠视觉暂留形成显示。难点在于DS1302读取时间和数码管刷新不能同时进行否则会丢数据。解决方案是用DMA配置TIM4的更新事件触发DMA把预存的段码数组自动写入GPIOB-ODRCPU全程不参与扫描。6.2 工业级扩展Modbus RTU主站开发打通“agile_modbus stm32”与PLC通信“agile_modbus”是一个轻量级Modbus库但直接移植到F103会遇到堆内存不足问题。它的modbus_t结构体默认malloc 256字节而F103的heap_size在startup文件里设为0x200512字节不够。必须修改modbus.h#define MODBUS_RTU_MAX_ADU_LENGTH 256 #define MODBUS_RTU_BUFF_SIZE 256 // 注释掉动态内存分配 // #define MODBUS_USE_HEAP // 改为静态分配 static uint8_t modbus_buffer[MODBUS_RTU_BUFF_SIZE];然后在modbus_new_rtu()里把mb-ctx-buffer指向modbus_buffer。Modbus RTU通信要求严格的波特率精度F103的USARTDIV计算公式为DIV ((25 * (USARTDIV 0x0F)) (USARTDIV 4)) / 16。例如115200bps在72MHz APB1下理论DIV72000000/(16*115200)39.0625取整为39实际波特率误差为|(39.0625-39)/39.0625|≈0.16%可接受。但若用HSI8MHz作时钟误差会超3%必须换HSE。6.3 毕业设计避坑指南为什么“基于stm32的智能台灯”项目要放弃PWM调光改用可控硅过零检测“基于stm32的智能台灯”看似简单但涉及强电
返回列表