尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

基于STM32的自动售货机完整实战:从状态机到RTOS再到稳定量产

基于STM32的自动售货机完整实战:从状态机到RTOS再到稳定量产 简介本资源是一套基于STM32F10x系列微控制器的自动售货机嵌入式系统完整源码工程面向嵌入式初学者、高校电子类课程设计学生及STM32项目实践者解决无人值守商品销售系统的软硬件协同开发问题。压缩包共229个文件含40余个C源文件实现外设驱动、状态机逻辑与支付交互、42个头文件h与目标文件o以及Keil MDK工程核心文件uvprojx、uvoptx、启动代码startup_stm32f10x_md.s、链接脚本与调试配置等完整覆盖从底层寄存器配置如RCC、USART、TIM、ADC、I2C到应用层业务逻辑货道控制、库存管理、用户界面响应的全栈实现。资源包大小为8.09MB结构清晰模块划分明确便于理解STM32标准外设库在真实工业场景中的组织方式。目前已有66人学习下载读者可直接编译烧录运行深入掌握中断处理、传感器信号采集、电机驱动时序控制及多任务状态切换等关键技术是开展课程设计、毕业设计或二次定制开发的高实用性参考工程。 前阵子把这个“基于STM32的自动售货机.zip”工程从硬盘里翻出来从头又跑了一遍顺便把散在各处笔记里的坑统一整理成这篇文章。这个项目以前是给学校旁边一个无人售货柜做原型时攒下来的后来拆给别人复用过几次反馈最多的问题反而不是算法难而是有些驱动细节和状态切换的边界没讲清楚。这次我按自己重新实现时会走的顺序从工程结构、硬件选型、状态机设计、驱动层落坑、RTOS任务拆分、联网上报到整机稳定性一层层把东西说透适合已经会点STM32基础、想完整做一个落地项目的朋友参考。项目不复杂整机逻辑就那些投币、选货、出货、找零、上报数据。但真正做起来你会发现坑全藏在驱动层和状态切换的边界里比如串口不定长帧被拆包、电机卡货检测不及时、SWD不小心被禁用、掉电之后库存状态丢失等等。所以这篇不止是讲“能跑”更多是讲“怎么跑得稳”。1. 拿到工程包之后怎么不迷路1.1 工程目录为什么要这么分打开ZIP之后别急着点编译先看目录结构。这个项目用的是“驱动层和应用层分离”的思路和很多教学代码相比最大的区别就是你不必为了改一个显示逻辑去翻整包驱动代码。我当时把目录收敛成了这几块Project/ ├── Core/ // 启动文件、时钟配置、NVIC、SysTick ├── Drivers/ │ ├── CMSIS/ │ └── STM32F1xx_HAL_Driver/ ├── BSP/ │ ├── uart_drv.c // 串口驱动含DMA接收框架 │ ├── adc_drv.c // 多通道ADC扫描DMA │ ├── motor_drv.c // 电机/继电器控制、光耦隔离逻辑 │ ├── display_drv.c // TM1650数码管 / OLED接口层 │ ├── tm1650.c │ ├── ina219.c // 电流电压监测 │ ├── esp8266_drv.c // WiFi模块AT指令封装 │ └── dfu_flash.c // 内嵌Flash读写 ├── Middlewares/ │ └── FreeRTOS/ ├── App/ │ ├── state_machine.c // 自动售货机状态机 │ ├── payment_protocol.c // 硬币/纸币支付模块协议解析 │ ├── task_ui.c │ ├── task_network.c │ └── watchdog_guard.c // 多任务看门狗喂狗机制 └── Doc/ ├── 硬件接线表.md ├── 支付协议说明.md └── 服务器上报协议.md这样分的好处是你在调试“售货流程”时基本只需要看App和BSP接口驱动内部就算有bug也只改BSP那一层不会被其他业务逻辑污染。对新手而言最直观的习惯就是别把协议解析、电机控制、屏幕刷新全揉进一个文件里。我见过有人把所有代码塞进main.c最后改一个引脚映射都要全局搜索半天那不是在写产品是在给自己挖坑。1.2 开发环境与下载配置这个项目基于STM32F103RCT6开发IDE用的Keil MDK 5。如果是第一次在电脑上编译顺序很重要先安装Keil MDK 5版本建议5.27以上然后装STM32F1系列的器件支持包否则编译器会报“device not found”如果ZIP里带了.svd或PDSCCoreInstaller直接双击就能装打开模板工程确认工程选项里芯片型号是STM32F103RC或者你实际用的那款配置下载器我用的ST-Link/V2在“Debug”选项卡里选“ST-Link Debugger”并打开“Flash Download”擦写算法。这里特别提醒一句话第一次下载之前先在Target选项里勾上“Reset and Run”。不勾的话程序烧完不会自动跑很多人会误以为固件没烧进去。1.3 芯片连不上的常见解法SWD被禁用的恢复流程这是我在这个项目里踩过的最大一次坑。早期版本为了让PB3复用做退币电机控制我在初始化时把JTAG功能直接关了只留SWD。后来调试中又想把SWD引脚也腾出来接别的外设结果代码烧进去之后第二次下载彻底连不上ST-Link Utility报错“No target connected”。原因很简单PA13/PA14是SWDIO/SWCLK它们缺省是调试功能但你一旦把这两个引脚配置成普通GPIO复用输出调试接口就失联了。如果你也砸在这个上面恢复流程是按住板子上的复位键不放打开STM32 ST-Link Utility选择“Target - Connect under reset”一直按着复位直到软件提示连接成功点“Target - Erase Chip”把Flash擦空松开复位重新下载正确固件。这个“under reset”的原理是Flash内容和启动流程可能已经破坏调试引脚但复位期间芯片还有短暂时间能被调试器接管。以后在设计代码时只要不是量产级限制永远别把SWCLK/SWDIO重映射掉。真缺引脚就从硬件层换更大封装的芯片省得拿稳定性去换引脚数量。2. 自动售货机需求拆解与硬件选型2.1 功能需求拆解自动售货机听起来是个整机机械但真正的逻辑可以拆成下面这些需求点。这个表我们项目里一直贴在调试桌前所有设计都能对回这张表。功能主项子功能实现方式商品管理多货道、库存计数、商品价格按键选择 数码管/OLED显示 Flash计数支付模块硬币识别 / 纸币接收 / 移动支付串口协议对接支付模块解析结果出货控制电机控制、到位检测、卡货报警光耦隔离驱动电机 限位开关 电流检测找零逻辑计算找零、退币控制状态机积分 退币电机控制状态显示当前金额、货道状态、错误提示TM1650数码管 / OLED / 可选LVGL彩屏数据上报销售记录、设备状态、远程重启ESP8266 HTTP/MQTT上报到后台故障恢复断电恢复、卡货重试、看门狗复位备份数据到Flash 独立看门狗 恢复流程有了这个表你在选芯片和外围硬件时心里就有底了。这个项目最终选择的方案是主控STM32F103RCT6256KB Flash、48KB RAM外设数量足够跑个简单RTOS 网络协议栈富余显示TM1650四位数码管显示金额一块0.96寸OLED做商品信息和故障提示支付串口协议的单硬币/纸币识别模块支持扫码枪串口输入也预留位置出货N20减速电机方案带霍尔码盘外接光耦限位反馈联网ESP8266-01S通过UART2接入后台用HTTP协议上报JSON数据电流监测INA219模块采样电机电流和总电源电压用于卡货检测。2.2 芯片选型为什么不选F407而是F103很多人看到“售货机要支持LVGL彩屏”就开始上F429、F407结果成本和功耗都上去了。实际我的经验是如果整机只做数字和文本显示那么带一个老式的TM1650数码管就够F103C8T6都行。但考虑到要挂ESP8266、跑状态机、后续要扩展扫码付和遥控出货我选了RCT6。理由资源足够256KB Flash能同时放FreeRTOS、HAL库和业务逻辑不需要精简到抠字节外设数量适中3个USART分别接支付模块、ESP8266和调试串口不会挤在一起社区资料多F103是“国民级芯片”任何驱动问题都能搜到现成答案量产风险低通过内嵌Flash记录库存状态不需要外挂EEPROM也能撑得住项目生命周期Flash擦写寿命至少1万次售货机定期记录库存足够。2.3 硬件接线表下面这个表是当时BSP驱动和原理图一一对应的最终版参考时可以按自己板子调整。信号MCU引脚连接对象备注电机1 控制PB0光耦输入驱动MOS管控制货道电机低有效电机2 控制PB1光耦输入驱动MOS管用于第二个货道或退币电机投币/支付串口PA9/PA10USART1 TX/RX接硬币识别器/扫码枪ESP8266串口PA2/PA3USART2 TX/RX接ESP8266模块的RX/TX调试串口PA13/PA14SWD只做调试不接任何其他外设TM1650 CLKPB6TM1650 CLK模拟I2C时用TM1650 DIOPB7TM1650 DIOOLED I2CPB8/PB9SSD1306 SCL/SDA如果使用硬件I2C则复用另配按键行0PC0按键1选货A上拉输入按下有效按键行1PC1按键2选货B按键行2PC2投币/确认限位开关0PB12电机0到位信号输入捕获或外部中断限位开关1PB13电机1到位信号INA219 SDAPB11INA219 SDAI2C1配合SCL PB10INA219 SCLPB10INA219 SCL这张表值得你花一天时间对着自己的原理图过一遍特别要注意引脚冲突。比如PB3、PB4、PA15、PB13在F103上缺省是JTAG相关引脚如果你要用它们做普通IO就必须在SystemInit时做“AFIO重映射”否则上电后它们默认是JTAG功能电压电平可能不对。当时我为了省一个GPIO把PB3映射成普通引脚输出退币脉冲结果调试接口异常后来干脆放弃复用多用一个IO也值。2.4 光耦隔离为什么必须加电机的瞬间启动电流很容易拉到1A甚至更高如果和STM32共地且中间不加隔离电机启停时反电动势会在地线上产生尖峰导致单片机复位或死机。这个项目里所有电机驱动信号都经过EL357N光耦单片机侧输出一个低电平光耦导通后驱动ULN2003再驱动电机。整个链路是STM32 PB0 → 限流电阻 1K → EL357N 输入侧 EL357N 输出侧 → ULN2003 输入 → ULN2003 输出 → 电机而且电源部分特别注意电机要用单独的12V适配器或DCDC转换不要和MCU共用3.3V。退币电磁铁和电机一样必须并联续流二极管。我在初期调试时没接续流二极管一按退币按钮主板就直接复位加上二极管后问题消失。3. 控制核心状态机设计让售货逻辑不再乱3.1 状态划分自动售货机的业务逻辑其实就是一个有限状态机。我见过很多人用“if加变量”的方式做比如在一个定时中断里判断“如果投币了并且选了货”这种代码在演示时能跑但一旦进入异常分支就乱了。所以我从一开始就把状态机写得非常明确。定义如下typedef enum { ST_IDLE 0, // 空闲待机 ST_ACCEPT_COIN, // 投币/扫码支付中 ST_WAIT_SELECT, // 已支付等待选择商品 ST_DISPENSING, // 出货中 ST_DISPENSE_DONE, // 出货完成等待确认 ST_CHANGE_RETURN, // 找零/退币中 ST_FAULT, // 故障 ST_CLOSE // 关门/停用 } vending_state_t;状态转移的核心原则是每个状态都有明确的进入条件、执行动作、退出条件和超时处理。绝对不能出现“某状态内两个事件同时触发”导致状态不知道往哪儿转必须在进入状态时把可能发生的事件按优先级排序。3.2 状态机主循环实现在裸机时我直接在主循环里调用状态机函数后续跑FreeRTOS后把它挂到独立任务里。核心代码如下void vending_state_machine(void) { switch (current_state) { case ST_IDLE: if (payment_event.amount 0) { current_money payment_event.amount; current_state ST_ACCEPT_COIN; display_show_money(current_money); } break; case ST_ACCEPT_COIN: // 持续接收支付事件累加金额 if (payment_event.amount 0) { current_money payment_event.amount; display_show_money(current_money); current_state ST_WAIT_SELECT; } if (current_money 0) { current_state ST_IDLE; } break; case ST_WAIT_SELECT: if (select_event.goods_id ! 0xFF) { if (stock[select_event.goods_id] 0) { show_error(No Stock); current_state ST_CHANGE_RETURN; // 全额退款 } else if (goods_price[select_event.goods_id] current_money) { show_error(Not Enough); current_state ST_CHANGE_RETURN; } else { current_goods select_event.goods_id; current_state ST_DISPENSING; motor_start(current_goods); start_dispense_timer(5 * 1000); // 5秒出货超时 } } break; case ST_DISPENSING: if (motor_done_event true) { // 出货成功扣库存 stock[current_goods]--; save_stock_to_flash(); current_money - goods_price[current_goods]; current_state ST_CHANGE_RETURN; // 计算找零 } if (dispense_timeout) { show_error(Stuck); motor_stop(); report_fault(FAULT_STUCK); current_state ST_FAULT; } break; case ST_CHANGE_RETURN: if (current_money 0) { return_change(); current_money 0; } current_state ST_IDLE; break; case ST_FAULT: // 需要人工介入或远程解锁 if (fault_recover_event) { current_state ST_IDLE; } break; default: current_state ST_IDLE; break; } }这段代码逻辑很简洁但实际要稳定跑起来关键点在于“事件”不是全局变量裸奔而是由队列或信号量递交给状态机。我在裸机阶段用了一个简单的环形队列在RTOS阶段改用FreeRTOS队列事件来源有按键、支付串口、限位开关、超时定时器。3.3 出货检测与卡货处理限位开关和电流检测缺一不可出货检测不能只靠“电机转了多久”因为电机可能空转或卡死。我采用双保险限位开关或红外对射检测货物是否落到取货口电机电流检测通过INA219或ADC采样判断电机是否堵转。限位开关的接线很简单但实现上有细节开关在电机一开始运动的瞬间会有一个明显的抖动如果直接用外部中断计数会把抖动当到位信号于是我在驱动层加了20ms软件消抖或者用定时器输入捕获做滤波。电流检测方案更可靠。N20减速电机正常运转时电流大约几十毫安如果货物卡住电流会瞬间升到几百毫安。在INA219驱动里我给每个货道设了一个“正常运行电流范围”出货过程中每隔100ms采样一次连续出现“超上限3次”就认为卡货立即关电机并进入故障流程。由于电流值受电压波动影响我当时在电机待机时先做了5次电流采样求平均值作为基线再设定阈值。3.4 投币和找零逻辑投币模块通过串口返回硬币面额或纸币金额比如协议里可能是“0x10表示一元硬币0x20表示五元纸币”。支付协议解析不能只按单字符处理因为串口帧可能被拆包所以必须用“帧头 长度 数据 校验”的不定长解析这部分在第4节驱动里会详细讲。找零逻辑更简单也更麻烦。简单是指如果使用微信/支付宝扫码支付平台已经帮你完成了找零麻烦的是如果用户投的是5元、10元硬币而商品只要2元你必须有找零机构。这个原型里我设计了“退币电机”通道通过状态机计算差值然后按面额依次退币。退币过程中会检查退币口的脉冲计数也走了一遍超时保护防止退币机构卡住导致用户没钱可退还要找客服。4. 驱动层实战串口、ADC、定时器、屏幕和几个经典坑4.1 串口不定长数据接收别再用逐字节中断支付模块和扫码枪都是通过串口发不定长帧比如硬币识别器会在一枚硬币落下后发“AA 55 01 00 02”纸币模块在扫码成功后发“AA 55 03 00 07”。如果你用HAL_UART_Receive_IT逐字节收每个字节都触发一次中断不仅CPU占用高而且还要自己折腾帧超时判断。这个项目里我用了“HAL库的空闲中断 DMA”接收方案。在STM32CubeMX中配置串口时打开“DMA IDLE interrupt”然后在代码里#define PAY_RX_BUF_SIZE 64 uint8_t pay_rx_buf[PAY_RX_BUF_SIZE]; volatile uint16_t pay_rx_len 0; volatile uint8_t pay_rx_flag 0; void MX_UART1_Init(void) { huart1.Instance USART1; ... HAL_UARTEx_ReceiveToIdle_DMA(huart1, pay_rx_buf, PAY_RX_BUF_SIZE); __HAL_DMA_DISABLE_IT(hdma_usart1_rx, DMA_IT_HT); // 关闭半传输否则会触发两次回调 } void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size) { if (huart huart1) { pay_rx_len Size; pay_rx_flag 1; // 把数据转交协议解析层 payment_protocol_parse(pay_rx_buf, pay_rx_len); // 重新开始下一轮接收 HAL_UARTEx_ReceiveToIdle_DMA(huart1, pay_rx_buf, PAY_RX_BUF_SIZE); } }“空闲中断”的意思是串口在一段时间内没有收到新数据认为一帧结束这时DMA刚好把整帧缓冲区交给你。这个方案最直接的好处是你拿到的是一整帧数据不会在解析时遇到半个帧头或半个校验字节。4.2 多通道ADC扫描DMA采样这个项目里ADC采样不止一路至少要采电机电流、总电源电压、投币传感器的感应信号。如果是F103的ADC1我开启4个通道循环扫描并连续采样用DMA把结果搬到内存uint16_t adc_values[4]; HAL_ADC_Start_DMA(hadc1, (uint32_t*)adc_values, 4);注意点ADC转换结果是12位DMA传输数据宽度必须设为半字16位否则结果错位如果开启扫描模式通道顺序以CubeMX里的Rank顺序为准采样时间不能太短尤其电机电流信号干扰多最好把采样时间拉到55.5周期以上再软件平均处理。实测中我发现如果直接用ADC采电机电流在电机PWM变化瞬间会出现很大的尖峰因此我在INA219驱动里做了“多次采样取中间值”的滤波效果比简单平均值好得多。4.3 定时器捕获测频率/计数脉冲N20减速电机的霍尔码盘会输出频率信号。通过定时器输入捕获可以测频率从而知道电机转速。在STM32CubeMX里把TIM2_CH1配置为InputCapture直接模式然后开启捕获中断HAL_TIM_IC_Start_IT(htim2, TIM_CHANNEL_1);在回调里记录上升沿次数void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) { if (htim htim2) { pulse_count; last_pulse_time HAL_GetTick(); } }通过脉冲数能推算当前出货电机是否在转动如果电机使能后一段时间内完全没有脉冲大概率是卡货或者接线断了。这个逻辑比单纯电流检测更早发现问题因为电流检测要等堵转一段时间才能反应。4.4 显示方案数码管TM1650和OLED的取舍TM1650驱动很简单它本身是一个4位LED驱动芯片接口类似I2C但时序略不同我直接用GPIO模拟的驱动搜索“tm1650驱动程序 stm32”能找到很多参考。典型初始化void TM1650_Init(void) { TM1650_START(); TM1650_SendByte(0x48); // 系统寻址 TM1650_STOP(); TM1650_START(); TM1650_SendByte(0x68); // 显示控制 TM1650_SendByte(0x01); // 开显示最暗亮度 TM1650_STOP(); }OLED则用SSD1306如果是I2C接口使用硬件I2C1效率更高。实际项目里我把数码管显示金额OLED负责菜单和故障码互不抢屏。如果项目要升级成彩屏触屏我建议先评估一下内存。F103的RAM只有48KB跑不了一些高级GUI框架。当时我在另一块板子上试过LVGL至少需要F407或F429否则刷屏卡顿而且还要外扩SDRAM。对自动售货机来说数码管和OLED是性价比最高的组合。4.5 延时函数卡死和DWT精确定时很多人会遇到这个热词“stm32延时函数delay卡死”。我最早也踩过原因是在中断回调里调了HAL_Delay而HAL_Delay依赖SysTick中断SysTick优先级如果设置得比当前中断低uwTick永远不更新就死循环了。解决办法有两个不要在中断里调用HAL_Delay改用状态机或定时器使用DWT计数器做微秒级延时它和SysTick无关只要内核时钟在工作就能用。DWT初始化如下void DWT_Delay_Init(void) { CoreDebug-DEMCR | CoreDebug_DEMCR_TRCENA_Msk; DWT-CYCCNT 0; DWT-CTRL | DWT_CTRL_CYCCNTENA_Msk; } static inline void DWT_Delay_us(uint32_t us) { uint32_t start DWT-CYCCNT; uint32_t ticks us * (SystemCoreClock / 1000000); while ((DWT-CYCCNT - start) ticks); }如果你跑FreeRTOSI2C、SPI的时序空闲等待也建议用DWT不要依赖HAL_GetTick()的1ms分辨率因为很多外设时序需要微秒级精度。4.6 SWD被禁用之后的修复方法前面第1节讲过恢复流程这里再补充一个预防建议在写初始化代码时不要让PA13、PA14、PA15、PB3、PB4脱离调试模式除非到了最终生产阶段经过验证。真遇到“第二次烧录连接不上”记住“Connect under reset”和“Erase Chip”这两步就够了。要是手头没有ST-Link也可以把BOOT0拉高用串口ISP方式重新烧一个新固件把Flash里的坏代码冲掉。5. 引入FreeRTOS任务划分、中断与共享数据5.1 为什么从裸机切到RTOS裸机状态机在主循环里跑得很顺直到有一天我发现三个问题同时出现ESP8266网络请求卡住时出货电机也在等超时OLED刷新被支付串口中断打断显示数字和后台上报数据偶尔错乱。这些问题本质上是“并发”导致的裸机主循环没办法优雅解决所以我把工程迁移到了FreeRTOS。不需要“为了用而用”但如果项目有多个异步外设需要同时处理RTOS能让你把每个业务拆成独立任务清晰很多。5.2 任务划分和优先级这是我在这个项目里的任务规划表你可以直接参考任务名功能说明优先级栈大小触发方式vending_task自动售货机状态机61024等待事件队列不轮询payment_task串口支付协议解析5512收到串口空闲中断后通知ui_task数码管/OLED刷新、按键扫描4512每50ms执行一次network_taskESP8266、MQTT/HTTP上报21024事件触发或定时上报watchdog_task监听各任务心跳并喂狗7256每200ms执行一次状态机任务优先级最高因为出货流程不允许被网络任务卡住。ui_task的优先级比pm_task低一个档次这样支付时实时性优先。5.3 中断里只做“通知”不要做业务FreeRTOS的黄金法则是中断回调里绝对不要调用非ISR安全函数。比如串口空闲中断里可以先调用xQueueSendFromISR把事件发送到支付队列让payment_task去解析这样中断执行时间极短。同理电机到位信号在外部中断里也只是xTaskNotifyFromISR一下具体电机停止逻辑由状态机任务处理。示例void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size) { BaseType_t xHigherPriorityTaskWoken pdFALSE; if (huart huart1) { xQueueSendFromISR(payment_evt_q, (void*)pay_rx_buf, xHigherPriorityTaskWoken); portYIELD_FROM_ISR(xHigherPriorityTaskWoken); } }5.4 共享资源和互斥量OLED和TM1650虽然都归ui_task管但支付任务偶尔也想显示金额这时最稳妥的办法是把显示操作封装成函数然后所有任务都通过同一个互斥量去调用。比如osMutexAcquire(display_mutex, osWaitForever); LCD_ShowString(0, 0, SALE); osMutexRelease(display_mutex);如果没有这个互斥量支付任务在更新“当前金额”的同时ui_task正在刷新数字就可能出现半新半旧的显示内容。对于调试串口也一样建议做一个带锁的printf_wrapper否则多任务同时打印日志会交错成一坨。6. 设备联网与远程数据上报6.1 用ESP8266还是直接用W5500自动售货机远程数据上报最省事的方案是ESP8266-01S。它成本低、协议简单只要用AT指令就能连上Wi-Fi。缺点是占用一个UART而且AT指令是异步的响应时间不可控所以我在网络任务里单独维护了AT指令状态机不能简简单单用阻塞式的HAL_Delay等待响应。后面在另一个版本里我也用过W5500有线网口方案。如果售货机旁边有网线接口W5500更稳定数据不会因为Wi-Fi信号波动丢失。但由于功耗和布线成本最终原型仍以ESP8266为主。6.2 简易HTTP协议封装我后台用的是一个轻量级HTTP服务设备端需要把销售记录以JSON格式POST上去。由于STM32资源有限我直接手写一个极简HTTP POST代码段也能配合cJSON库做序列化。char http_cmd[512]; cJSON_Delete(cJSON_pay);例子不展开核心点是先通过ESP8266 AT指令连接服务器IP和端口再发送POST请求头、Content-Length、JSON body等待服务器返回HTTP 200如果不能联网就把销售记录存入内部Flash队列等下次联网后补报。6.3 每个STM32都有唯一ID别浪费热词“stm32每一个芯片有没有类似id或者mac地址”这个问题答案是有。STM32内部有一个96位唯一标识不同芯片一定不同。在F103上地址是0x1FFFF7E8读取方式uint32_t uid[3]; uid[0] *(volatile uint32_t*)0x1FFFF7E8; uid[1] *(volatile uint32_t*)0x1FFFF7EC; uid[2] *(volatile uint32_t*)0x1FFFF7F0;我在这个项目里用这三个DWORD拼接出一个16字节的字符串作为设备序列号后台注册设备时会校验这个ID防止有人伪造数据上报。如果你有需求这个ID也能做成设备证书或者加密密钥的种子。6.4 异常上报与断电恢复联网最核心的价值不是实时显示“还有几瓶可乐”而是故障报警。当售货机发生卡货、空转、开箱告警时网络任务需要立刻上报一条事件。如果断电发生在出货过程中必须在恢复后从Flash里读取上次未完成出货的标记重新进入恢复流程或者至少报给后台“设备上次在出货中掉电用户可能未完成购买”。Flash存储要小心不要直接无脑循环写同一个地址否则很快磨损。我在驱动层做了简单的偏移管理把库存、销售日志、掉电标记分散写到不同区域每次写之前先擦除整个扇区。批量量不大这个方案可行。7. 整机稳定性与现场排查清单7.1 看门狗不能只在一个地方喂很多项目的看门狗是在主循环末尾HAL_IWDG_Refresh看起来简单但其实如果某个子任务死循环主循环可能仍然在跑看门狗根本发现不了问题。我采用的做法是每个RTOS任务定期更新一个全局或共享变量作为心跳看门狗任务检查所有心跳是不是都在最近1秒内更新过只要有一个超时就主动触发一次软件复位。这样能确保“死循环或卡死”的任务也能被识别。typedef struct { uint32_t vending_beat; uint32_t ui_beat; uint32_t network_beat; } TaskBeat; void watchdog_task(void *argument) { for (;;) { if ((HAL_GetTick() - task_beat.vending_beat 1000) || (HAL_GetTick() - task_beat.ui_beat 1000) || (HAL_GetTick() - task_beat.network_beat 3000)) { // 记录复位原因然后软复位 report_fault(FAULT_WATCHDOG_TIMEOUT); NVIC_SystemReset(); } HAL_IWDG_Refresh(); osDelay(200); } }7.2 电流电压异常保护INA219模块很方便它通过I2C读取分流电阻上电压和总电压。我在驱动里加了一句“软件慢启动”电机启动前先把电源输出拉低300ms再逐渐加PWM占空比而不是直接满占空比启动这样可以避免启动浪涌电流导致电压跌落触发自己的过压或欠压保护。7.3 现场联调测试清单给各位参考一份我最后交付前的自测表每一项都真实跑过测试项测试方法预期结果正常购买流程投币1元选择2元商品提示余额不足退币正常购买流程投入5元选择2元商品出货成功找零3元库存为0把某货道库存改成0再选择显示“No Stock”退回金额卡货检测用螺丝刀卡住电机输出轴触发购买电机停止上报故障网络断线断开Wi-Fi后售出5单销售记录暂存联网后自动补报掉电恢复出货中突然断开电源重新上电设备进入恢复流程不丢库存看门狗故意注释某个任务的喂狗代码跑30分钟设备自动重启后台收到复位记录退币异常拆掉退币电机再触发找零报退币故障不吞用户余额7.4 调试技巧和心得调试串口加上时间戳日志里能看到每个事件在哪个Tick发生排查串口乱帧和电机时序问题特别方便。INA219电流采样结果有时候会跳变别直接拿单次值做逻辑判断做移动平均。我当时用了一个长度为16的环形缓冲区稳定很多。OLED刷新不要放在高优先级任务里否则会拖慢状态机。ui_task里刷新一次屏幕大概需要几十毫秒如果用SPI刷屏更慢建议把显示刷新和按键扫描拆成两个独立小函数错开执行。在现场调RTT或printf时如果在RTOS里输出日志建议用互斥量包一层否则日志会被其他任务切成两半。最后分享一个我在实际调试中养成的习惯出货前记一条事件出货后再记一条结果这两条日志之间如果时间超过了阈值就说明货道被卡或限位开关有问题。有了这个习惯售后反馈的“偶尔不出货”问题基本都能靠日志定位而不是靠现场反复按按钮碰运气。这个项目整体难度不高但细节决定产品能不能从实验室走向街头。本文还有配套的精品资源点击获取
返回列表