简介:一套基于STM32单片机的条形码扫描与识别系统毕业设计完整方案,适合嵌入式、物联网方向学生作为课设/毕设参考。资源包含原理图、源程序、开题报告、参考论文、元件清单及实物图,覆盖从硬件电路设计、传感器信号采集到解码算法与串口通信的完整开发链路。包内共302个文件,以C语言源文件、头文件、编译中间文件为主,同时提供Keil工程文件、原理图SchDoc、Hex烧录文件及多份Word/PDF文档,整体约30MB,目录结构清晰。已有926人学习下载。源码中可见STM32标准外设库的典型应用,涵盖RCC时钟配置、ADC模拟采集、I2C通信及定时器功能,可对照原理图理解条形码模块与主控的接线和调试过程。此外,开题报告与参考论文提供了设计思路、技术路线和测试方法,便于撰写毕业设计文档;元件清单与实物图则有助于快速采购、焊接和结果验证,显著降低从头搭建系统的门槛。
1. 条形码识别系统用什么单片机:STM32 定位毕设的合理边界
快递驿站的手持扫码枪三秒扫一个件,拆开细看会发现一台扫码设备分两层:光学引擎把黑白条宽解码成字符串,STM32 接手后半段——收串口、校验、显示、上传。毕业设计题为“基于 STM32 单片机条形码扫描器&条形码识别系统设计”,交付物除了源程序,还有原理图、开题报告、元件清单与实物图。常见做法是用 STM32F103 接一只串口型扫描引擎模组,把风险最高的光学译码交给引擎,MCU 专注数据业务,两周内就能跑通演示。下文按条码原理 → 原理图 → 固件 → 业务层 → 调试展开,主攻串口帧接收、EAN-13 校验位和电平匹配三个卡点,覆盖答辩时最常被追问的设计依据。
2. 条形码识别原理与 STM32 选型:从条宽到比特流
2.1 EAN-13 与 Code128 的结构差异:解码器到底在解什么
一维条形码的本质是把“数字或字符”映射成一组宽度不同的黑条和白条交替序列。以最常见的 EAN-13 为例,共 13 位数字,前 12 位是数据,第 13 位是校验位。左侧 6 位数字使用奇偶两组编码,右侧 6 位使用统一的偶编码,每组编码由 7 个模块位宽组成,最左和最右的保护区宽度固定。解码器扫描到条空边界后,不是直接读“1”或“0”,而是测量相邻边界的宽度比,再按比例判定每个模块是 0 还是 1。宽度比的容错能力,决定了劣质打印的条码能不能被识别出来。
Code128 则是可变长、全 ASCII 的连续型条码,每个符号同样由 11 个模块构成,但内部通过 A/B/C 三套字符集切换来压缩信息。它比 EAN-13 灵活得多,物流面单、企业内部 SKU 常用它,但因为字符集切换和校验符计算方式不同,调试时不能复用 EAN-13 的校验代码。
/* 宽度序列转模块bit:假定扫描器已输出相邻条空宽度数组 */ uint8_t Widths_To_Bits(uint16_t *width, uint8_t n) { uint16_t min_w = width[0]; for (uint8_t i = 1; i < n; i++) { if (width[i] < min_w) min_w = width[i]; } uint16_t unit = (min_w * 3) / 2; /* 容忍1.5倍最小宽度 */ uint8_t bits = 0; for (uint8_t i = 0; i < n; i++) { bits = (bits << 1) | (width[i] > unit ? 1 : 0); } return bits; }这段代码用最小宽度的 1.5 倍做阈值,把一组脉冲宽度压成 0/1 比特流。真正产品级解码器还要处理首尾噪声、比例漂移和拼接扫描,但在课程设计和毕设中,掌握“宽度比例 → 模块比特 → 查表取字符”这三层,基本就能讲清原理。注意这段代码只在拿到“条空宽度数组”时才成立,假如你用的是串口扫描引擎模组,这一步已经被引擎完成了。
2.2 72MHz 主频的 STM32F103 够不够:资源盘点
串口型扫描引擎的标准输出是 ASCII 字符串加回车符,数据量很小,一帧 EAN-13 只有 14 个字符(13 位数字加 0x0D)。这个体量对 STM32F103C8T6 来说非常轻松:72MHz Cortex-M3 内核、64KB Flash、20KB RAM,两路 USART 分别接扫描头和上位机,DMA1 负责串口接收搬运,TIM2 做外部计数或定时去抖,I2C1 接 OLED,整体外设余量在 60% 以上。
选型时要区分“扫码头”和解码算法的两个层次。如果题目要求自带图像识别,比如 OV7670/OV2640 摄像头模组直接送图像到单片机做条码定位与解析,那 STM32F103 的算力就非常吃紧,Flash 也放不下带浮点运算的成熟库。常见做法是改用 K210 这类带 KPU 协处理器的视觉 MCU 做识别,再用串口与 STM32 通讯完成业务控制。这个组合在“基于 STM32 的毕业设计”里很常见,也更容易讲出系统设计的感觉。
我一般建议直接选串口引擎方案:系统复杂度低、实物图好看、答辩链路完整。扫描引擎负责光学和译码,STM32 从串口拿到的是干净字符,自己只需要处理解析、校验、显示与上传,正好避开光学模组最高风险的 60% 工作量。成本上,引擎模组比 CMOS 方案贵二十元左右,但换来的是稳定性和开发进度。
2.3 扫描方案二选一:串口扫描引擎与 CMOS 图像自解
| 对比项 | 串口扫描引擎 | CMOS 摄像头 + MCU 自解 |
|---|---|---|
| 数据形态 | 已解码 ASCII 字符串 | 原始图像 |
| STM32 负载 | 串口收帧、校验、上传 | 图像二值化 + 条空分割 + 译码 |
| 典型芯片需求 | STM32F103 足够 | STM32F4/H7 或 K210 协处理 |
| 调通周期 | 1~3 天 | 2 周起步 |
| 毕设展示效果 | 实际扫码出字,稳定 | 可演示识别过程,风险高 |
| 成本 | 中 | 低 |
选串口引擎时,注意引擎工作电压和输出电平。老式激光模组有 5V TTL,新的红光引擎普遍 3.3V,购买前问清楚“TTL 电平、无 PS2 键盘口、带回车尾缀”。5V 引擎接 3.3V STM32 也不是不能用,但 TX 方向需要电阻分压或电平转换,原理图上多两颗电阻,这部分下一章展开。
3. 条形码扫描器原理图设计:STM32 与扫描引擎的电气连接
3.1 最小系统与电源骨架:3.3V 轨上要注意的电容
常见做法是 5V 输入,USB 取电或 5V DC 座进入,经过 AMS1117-3.3 稳压到 3.3V。AMS1117 的最大压差约 1.1V,输入 5V 输出 3.3V 时纹波不大,但输入输出电容不能省:输入放 10uF/16V 电解电容,输出放 10uF 钽电容加 0.1uF 陶瓷电容。
STM32 每个电源引脚(VDDA、VDD)就近放一个 0.1uF 去耦电容,这是最简单的 EMI 抑制手段。扫描引擎工作瞬间电流接近 100mA,3.3V 轨上再并一个 47uF 电容,避免扫码瞬间 OLED 亮度抖动。晶振部分,如果用 8MHz 无源晶振,匹配电容按负载电容公式计算。以 8MHz、CL=20pF 的晶振为例,两边各放 20pF 对地电容,对应“STM32 晶振电容计算”里最常见的选值,改 25MHz 晶振时倍频系数也要同步调整。
/* CubeMX 生成的 RCC 配置片段,只保留关键参数 */ void MX_RCC_Init(void) { RCC_OscInitTypeDef RCC_OscInitStruct = {0}; RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE; RCC_OscInitStruct.HSEState = RCC_HSE_ON; /* 使能8MHz外部晶振 */ RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON; RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE; RCC_OscInitStruct.PLL.PLLMUL = RCC_PLLMUL9; /* 8MHz * 9 = 72MHz */ HAL_RCC_OscConfig(&RCC_OscInitStruct); }这里 PLLMUL9 是 F1 系列的典型配置:HSE 8MHz 倍频 9 倍到 72MHz。如果换成 12MHz 晶振,倍频数改成 6 倍;晶体型号变了,代码里的 PLLMUL 也得改,否则系统时钟异常,串口波特率直接偏掉。所有 USART 通信的乱码问题里,时钟源配错是仅次于波特率不匹配的第二大原因。
3.2 串口扫描引擎的接线:TTL 电平的握手细节
原理图上扫描引擎和 STM32 的连接是两根数据线加一根地线:引擎 TXD 接 STM32 RXD,引擎 RXD 接 STM32 TXD,电源 VCC 接 3.3V,GND 共地。这里常常不是“同名直连”而是交叉连接,新手画原理图最容易把 TX 对 TX 连在一起。
另外两件事:如果引擎是 5V TTL,RXD 方向(STM32 TX 到引擎 RX)可以用两个电阻分压,10K 串 + 6.8K 下拉,把 3.3V 高电平分到约 2.4V;更省事的方式是直接买 3.3V TTL 引擎,电气上干净很多。其次,数据线连接器旁放一颗 TVS 管做 ESD 保护,例如 SMAJ5.0A,再并一个 100nF 电容到地。对毕设来说不强求,但原理图评审时画上去会显得专业。
数据线布线原则是“远离 8MHz 晶振与电源电感”,F103 没有高速外设,布线的容错率很高。真正要留意的是扫码器供电地和 STM32 地之间的单点接地,不要在 USB 地和扫描头地之间形成环路。实物连接时如果扫描头供电来自同一个 5V 电源,GND 必须先连通再上电,拔插顺序错误容易烧扫描头内部芯片。
3.3 外设展开:OLED、按键、蜂鸣器怎么挂
3.3.1 I2C OLED 的地址与上拉电阻
常用的 0.96 寸 SSD1306 OLED,I2C 地址要么是 0x3C 要么是 0x3D,取决于模块背后 SA0 电阻。STM32 的 I2C1 是开漏输出,规范上要求 4.7kΩ 上拉电阻到 3.3V,不过很多 OLED 模块 PCB 上已经带了上拉,如果你的开发板用的是 F103C8T6 最小系统板,板上通常也集成了 4.7K 上拉。直接在模块上量 SCL 电压,若已接近 3.3V 就不用重复加。
/* SSD1306 初始化序列的关键命令,I2C 地址 0x3C */ static const uint8_t ssd1306_init_cmds[] = { 0xAE, 0xD5, 0x80, 0xA8, 0x3F, 0xD3, 0x00, 0x40, 0x8D, 0x14, 0x20, 0x00, 0xA1, 0xC8, 0xDA, 0x12, 0x81, 0xCF, 0xD9, 0xF1, 0xDB, 0x40, 0xA4, 0xA6, 0xAF }; void OLED_Init(void) { HAL_I2C_Mem_Write(&hi2c1, 0x3C << 1, 0x00, 1, (uint8_t *)ssd1306_init_cmds, sizeof(ssd1306_init_cmds), 100); }这段代码用 HAL_I2C_Mem_Write 连续发送控制命令,0x00 表示目标寄存器是命令通道。注意地址参数要左移一位,HAL 库内部用 8 位地址格式,0x3C 必须写成 0x78。很多人移植 OLED 库时卡在这里:库函数里写的是 0x78,原理图上标的又是 0x3C,两边不是同一个数,就会出现“初始化黑屏,代码反复检查也找不到问题”的错觉。
3.3.2 蜂鸣器与指示灯:扫码成功的反馈通道
蜂鸣器用有源蜂鸣器,3.3V 或 5V 版本都可以,但驱动管不能直接接 GPIO。典型接法是 GPIO 通过 1kΩ 基极限流电阻驱动 NPN 三极管(S8050),集电极接蜂鸣器负极,蜂鸣器正极接 3.3V 或 5V,同时在蜂鸣器两端反向并联 1N4148 二极管吸收关断时的反向电动势。
| 引脚 | 功能 | 类型 | 备注 |
|---|---|---|---|
| PA2 | USART2_TX → 引擎 RX | 推挽输出 | 9600/115200 |
| PA3 | USART2_RX ← 引擎 TX | 浮空输入 | 上拉可减小噪声 |
| PB6 | I2C1_SCL → OLED | 开漏 | 上拉 4.7k |
| PB7 | I2C1_SDA → OLED | 开漏 | 上拉 4.7k |
| PB0 | 按键输入 | 上拉输入 | 触发手动重扫 |
| PB1 | 蜂鸣器驱动 | 推挽输出 | 低电平有效或高电平有效视接法 |
| PC13 | LED 指示 | 推挽输出 | 扫码成功闪 1 次 |
按键与蜂鸣器不建议共用引脚,虽然可以用分时复用省 IO,但代码里中断与蜂鸣器驱动耦合,调试时容易误触发。F103C8T6 的 IO 足够,按上表各自独立分配即可。原理图上所有输入类引脚都串一个 1kΩ 电阻作保护,输出类引脚尤其是驱动三极管的 GPIO,原理图评审时会看阻值是否合理。
4. 源程序核心:STM32 串口空闲中断 + DMA 接收条形码
4.1 扫描头一帧到底长什么样:ASCII 与回车终止
扫码引擎输出的是可打印 ASCII 字符串,一帧数据以回车 0x0D 结束。不同厂家的引擎可能带 0x0D 0x0A 双结束符,也可能在前面加前缀符(例如厂商代码)。实际数据格式可以归纳成下表:
| 引擎输出 | 对应含义 |
|---|---|
| 13 位数字 + CR | EAN-13 / UPC-A |
| 起始符 B + 数据 + 校验 + 结束符 + FNC1 + CR | Code128 子集 B |
| “$” 前缀 + 9 位数字 | Code39 的扩展形态 |
判断一帧结束最可靠的办法,不是等固定字节数,而是等“总线空闲”。引擎扫码一次的数据是连续发出的,两个字节间隔小于 1ms,发完 CR 后总线立刻空闲。利用这个特性,空闲中断(IDLE)能精确地告诉 MCU“一帧发完了”,比在字符串里找 CR 更抗抖动。
4.2 用 IDLE + DMA 接收不定长条形码
4.2.1 初始化代码与参数说明
以 USART2 连接扫描引擎、波特率 9600 为例,CubeMX 里把 USART2 配置为异步模式,DMA 选择 DMA1 Channel5(USART2_RX),模式为 Circular(循环模式)。DMA 循环模式的意义在于:接收缓冲区的数据可以不断覆盖,不需要每次收完一帧就重新配置 DMA,只有读取数据时临时停一下 DMA。
#define BAR_RX_BUF_LEN 256 uint8_t bar_rx_buf[BAR_RX_BUF_LEN]; volatile uint8_t bar_rx_frame_ready = 0; volatile uint16_t bar_rx_frame_len = 0; void BSP_ScanEngine_Init(UART_HandleTypeDef *huart) { __HAL_UART_ENABLE_IT(huart, UART_IT_IDLE); /* 使能空闲中断 */ HAL_UART_Receive_DMA(huart, bar_rx_buf, BAR_RX_BUF_LEN); }__HAL_UART_ENABLE_IT使能空闲中断;HAL_UART_Receive_DMA启动 DMA 接收,DMA 把 USART2_DR 收到的每个字节搬进bar_rx_buf。缓冲区长 256,对一帧 20 字节以内的条形码绰绰有余,且避免环形索引计算。注意这里没有直接开HAL_UART_Receive_IT,因为逐字节中断在 115200 波特率下会大量打断主循环,DMA 省去 CPU 搬运,空闲中断只在一帧结束时触发一次。
4.2.2 空闲中断里计算长度
当接收完一帧、总线空闲时,硬件置 IDLE 标志并触发中断。中断回调里不能直接从头遍历缓冲区,因为 DMA 循环模式下,DMA 计数器递减表示剩余空间,用缓冲区长度减去剩余计数就是已收数据的长度。
void HAL_UART_IDLE_Callback(UART_HandleTypeDef *huart) { if (huart->Instance == USART2) { uint16_t dma_remain; __HAL_DMA_DISABLE(huart->hdmarx); /* 停住DMA,防止数据覆盖 */ dma_remain = __HAL_DMA_GET_COUNTER(huart->hdmarx); /* 剩余未接收字节数 */ bar_rx_frame_len = BAR_RX_BUF_LEN - dma_remain; /* 本次一帧长度 */ bar_rx_frame_ready = 1; /* 置位帧就绪标志 */ __HAL_DMA_SET_COUNTER(huart->hdmarx, BAR_RX_BUF_LEN); /* 重新填入满长度 */ __HAL_DMA_ENABLE(huart->hdmarx); /* 重新使能DMA */ } }关键点在__HAL_DMA_DISABLE和__HAL_DMA_SET_COUNTER的顺序。必须先关闭 DMA 再读计数,否则在读取瞬间新数据到了,计数值会跳到错误值;重新使能前必须把计数器恢复为缓冲区总长度,否则下一次 DMA 会从错误的位置开始写。bar_rx_frame_ready放 main 循环里轮询,处理完一帧后手动清零,避免在中断里做重活。另外,工程里的USART2_IRQHandler必须调用HAL_UART_IRQHandler(&huart2),HAL 库才会替我们清掉 IDLE 标志并回调当前函数。
主循环侧按下面方式取数据:
while (1) { if (bar_rx_frame_ready) { bar_rx_frame_ready = 0; uint16_t len = bar_rx_frame_len; uint8_t *data = bar_rx_buf; /* 去掉CR/LF等尾缀 */ while (len > 0 && (data[len-1] == '\r' || data[len-1] == '\n')) len--; Barcode_Parse(data, len); } }去尾缀这步很容易被忽略:如果直接按原长度解析,EAN-13 的 13 位字符后面多出 CR,字符串转数字时会把回车当成非法字符。先剥掉\r、\n,再做格式解析,是最值得养成的一步。
4.3 EAN-13 校验位与条形码格式校验
条形码解析的目标不是“显示字符串”,而是验证它确实是一个合法条码。EAN-13 用模 10 校验:从第 1 位到第 12 位,奇数位权重 1、偶数位权重 3,求和后取 10 的补数,就是第 13 位校验位。
/* 返回1表示校验通过,0表示失败 */ uint8_t Barcode_EAN13_Check(const uint8_t *buf, uint8_t len) { uint16_t sum_odd = 0, sum_even = 0, checksum = 0; if (len != 13) return 0; for (uint8_t i = 0; i < 12; i++) { if (buf[i] < '0' || buf[i] > '9') return 0; /* 非数字字符直接无效 */ if ((i & 1) == 0) sum_odd += buf[i] - '0'; /* 第1,3,5...位(从0开始)权重1 */ else sum_even += buf[i] - '0'; /* 第2,4,6...位权重3 */ } checksum = (10 - (sum_odd + sum_even * 3) % 10) % 10; return (checksum == (buf[12] - '0')); }这里用buf[i] - '0'把 ASCII 数字转成数值;(sum_odd + sum_even*3) % 10得到末位,10 - 末位取补。最后%10是为了处理恰好整除的情况,比如计算结果是 10 时校验位应为 0。如果引擎输出的数据带了厂商前缀,需要先把前缀剥离再调这个函数;Code128 的校验算法不同,业务上只做长度和字符集检验就够。到这里,DMA 收帧 → IDLE 定界 → 去 CR → 校验的数据通路已经闭环。
5. 条形码识别系统业务层:OLED 显示与数据上行
5.1 OLED 显示扫码结果与格式转换
解析成功后的第一件业务动作是显示。OLED 128x64 一屏最多显示 16 个字符(8x16 字体),一帧 EAN-13 去掉回车后是 13 个字符,刚好放得下;Code128 则可能超过 16 个字符,需要分两行显示。
void Display_Barcode(const uint8_t *data, uint16_t len) { char line1[17], line2[17]; uint8_t n1 = (len > 16) ? 16 : len; memcpy(line1, data, n1); line1[n1] = '\0'; OLED_Clear(); OLED_ShowString(0, 0, "BARCODE:", 1); OLED_ShowString(0, 2, line1, 2); if (len > 16) { uint8_t n2 = len - 16; if (n2 > 16) n2 = 16; memcpy(line2, data + 16, n2); line2[n2] = '\0'; OLED_ShowString(0, 5, line2, 1); } HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_RESET); Buzzer_Beep(60); /* 短鸣60ms */ }OLED_ShowString中间的参数是字号:1 表示 8x16、2 表示 16x32,不同库的参数含义不完全统一,移植时注意看函数头注释。这里用 memcpy 切出前 16 字与后 16 字,杜绝 OLED 库内部把\0当数据刷到屏幕上。LED 在显示成功后拉低点亮,蜂鸣器短鸣 60ms 作反馈——如果响得过长,真实扫码体验会显得“卡一下”。
5.2 数据上行到上位机:串口打印与 AT 指令透传
毕设演示时通常要求扫码结果能同步到电脑或手机端。最省事的做法是把 USART1 作为调试串口连接 USB 转 TTL,用 sprintf 拼一条固定格式的数据帧发出去,给上位机软件留好解析窗口。
void Uplink_SendBarcode(uint8_t slot, const uint8_t *data, uint16_t len) { char txbuf[64]; int n = snprintf(txbuf, sizeof(txbuf), "SCAN,%02d,", slot); for (uint16_t i = 0; i < len && n < (int)sizeof(txbuf)-1; i++) { txbuf[n++] = (char)data[i]; } txbuf[n++] = '\r'; txbuf[n] = '\0'; HAL_UART_Transmit(&huart1, (uint8_t *)txbuf, n, 1000); }snprintf先写槽位号,再把条形码逐个字符填入txbuf,最后补 CR。上位机按逗号拆分字段即可。若题目要求物联网接入,常见做法是让 STM32 通过 USART 接 ESP8266,用 AT 指令透传把缓冲区内容发到云端——AT 指令本身也是字符串协议,加一段AT+CIPSEND长度前缀即可,逻辑完全复用。参数取值可以参考下面的表:
| 参数 | 取值 | 说明 |
|---|---|---|
| 上位机波特率 | 115200 | 与 USB 转 TTL 模块一致 |
| 帧前缀 | SCAN | 便于上位机按逗号拆字段 |
| ESP8266 模式 | AT+CIPMUX=0 | 单连接模式,省内存 |
| 云端协议 | TCP/HTTP | 本代码只负责字节流,按需求二选一 |
5.3 去除重复扫描:定时器去抖与锁存逻辑
真实演示时最容易出现的尴尬场面是:同一件商品放在扫码区域,屏幕刷新了七八次,上位机收到一堆重复记录。这不是识别系统“坏了”,而是扫描引擎在连续采样条码。处理方法和按键去抖一样:把最近一次成功扫码的时间锁存,锁存期内忽略后续扫码。
static uint32_t last_scan_tick = 0; #define SCAN_LOCK_MS 500 uint8_t Barcode_TryLock(void) { uint32_t now = HAL_GetTick(); if (now - last_scan_tick < SCAN_LOCK_MS) { return 0; /* 锁存期内,直接丢弃 */ } last_scan_tick = now; return 1; }在Barcode_Parse入口调用这个函数,重复扫描的帧会被挡掉。反过来,如果某次扫码耗时长需要超过锁存时间,就把 500ms 调到 800ms,但别超过 1000ms,否则连续扫两件不同商品时会丢掉第二个。按键、步进电机启动这些场景都能用同一套“时间戳锁存”思路;STM32 的 SysTick 每毫秒中断一次,HAL_GetTick 在锁存逻辑里非常可靠,不用额外开定时器。这种用状态锁存区分业务的做法,本质上是状态机的雏形,写参考论文的“软件流程设计”一节时,这就是现成的素材。
6. 调试条形码扫描器:逻辑分析仪与串口参数双验证
6.1 先验证扫描引擎本身
固件还没写的时候,先把扫描引擎拿到手边。接一个 USB 转 TTL 模块,TXD 接引擎 RXD、RXD 接引擎 TXD、GND 共地。打开串口助手,波特率分别试 9600 和 115200,扫一张 EAN-13 条码。如果输出正常,说明引擎没问题,后面在 STM32 侧只需要做波特率匹配;如果输出乱码,几乎一定是波特率不匹配。两边都用 ASCII 模式查看,不要开 HEX 模式,否则一眼看不出结构。
6.2 逻辑分析仪抓 TTL 波形
串口助手验证通过后,在原理图和实物的临界点上加逻辑分析仪:探头夹在 STM32 的 PA3(USART2_RX)上,地接 GND,采样率 1MHz 以上。正常波形应看到空闲高电平 3.3V,起始位拉低,8 个数据位加停止位。如果 RX 线上没有任何跳变,查引擎供电和 TXD 是否真的在输出;如果有波形但 STM32 收不到,查 PA3 的 AF 复用配置是不是被 CubeMX 设成了普通 IO。
6.3 SWD 在线调试与缓冲区观察
接 ST-Link 后,在HAL_UART_IDLE_Callback里打断点并不合适:扫码是快事件,断点一卡 DMA 状态全变。更好的做法是在主循环的Barcode_Parse入口打断点,或直接在变量窗口查看bar_rx_buf[0..len]。配合 ST-Link Utility 可以读整个 RAM 区域的原始值,判断是接收长度不对还是数据内容不对。要注意的是,在线调试时 OLED 和蜂鸣器会拖慢主循环,影响“连续两帧间隔小于 1ms”的假设,所以调试时先断开 OLED 初始化,调通后再加回。
6.4 扫码链路故障排查表
| 现象 | 先查 | 再查 |
|---|---|---|
| 完全无输出 | 引擎 GND 是否与 STM32 共地 | RX 波形是否存在 |
| 数据显示为乱码 | 波特率 9600/115200 是否匹配 | 扫描引擎是 TTL 还是 RS232 电平 |
| 扫一次显示多次 | Barcode_TryLock是否在解析入口调用 | 引擎是否带“连续扫描模式” |
| OLED 黑屏 | I2C 地址 0x3C/0x78 是否写混 | 上下拉电阻是否缺失 |
| 蜂鸣器长响不停 | 驱动管基极电阻是否过大 | 有源蜂鸣器极性是否接反 |
这张表按信号流从物理层到应用层排列。实际排错时路线是固定的:先确认波特率匹配,再看 TX/RX 线序是否交叉,最后确认 IDLE 中断是否真正触发。三个底座确认完,剩下的业务逻辑问题大多在十分钟内定位。
本文还有配套的精品资源,点击获取