ARTICLE DETAIL

资讯详情

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

基于MSP430的三人/五人投票表决器设计与实现

基于MSP430的三人/五人投票表决器设计与实现 做单片机项目这些年投票表决器可以说是入门必做的小设计之一但大部分人的做法是用51单片机点亮几个LED就收工。这次我换了个思路用TI的MSP430系列来做并且把问题再往前推一步标题里写的是“三人投票表决器”但产品要求又提到“五人表决器”所以我干脆做成一套硬件结构兼容三人和五人的方案通过简单的配置就能切换表决人数。这篇博文会把整个项目从需求拆解、芯片选型、硬件电路设计到软件逻辑实现完整走一遍。适合正在学单片机的高校学生、准备电子设计竞赛的队伍以及想从51换到MSP430练手的工程师。你不需要有太多MSP430基础我会把IO配置、按键消抖、投票判定这些关键点讲清楚保证你能照着做出来。1. 项目概述从三人表决器到五人表决器的设计思路1.1 表决器到底要解决什么问题表决器的功能非常直白每位参与人有一个按钮投票时按下表示“同意”松开或者不按表示“不同意”。系统统计同意票数如果同意人数超过总人数的一半就判定“通过”否则“不通过”。比如三人表决时至少2人同意即通过五人表决时至少3人同意才通过。听起来简单的逻辑真正落地时有不少细节。比如按键按下的时候机械触点会产生抖动不去抖的话一次按下会被识别成好几次又比如投票结束后必须锁定结果不能允许有人中途改票再比如如果人数可变软件逻辑必须支持灵活配置而不是写死三个人。我把需求整理成了一个功能清单支持3人或者5人投票通过拨码开关选择人数模式。每个投票人有独立按键按下即同意未按即不同意。投票结束后由主持人按键锁定结果。通过时亮绿灯不通过时亮红灯数码管显示统计的同意票数。支持重新投票一键复位。这些需求实际来自一个课程设计题目但我觉得它完全可以做成一个实用的演示设备放在课堂上用来快速统计意见效果比举手投票直观得多。1.2 为什么选MSP430而不是51很多学校课程设计默认用STM32或者51因为资料多、上手快。但这次我特意选MSP430原因有几点第一MSP430是16位超低功耗单片机工作电流在微安级别用电池就能跑很久。表决器这类设备通常不是持续工作的大部分时间处于待机状态MSP430的低功耗模式非常适合。第二MSP430的IO口数量足够。三人模式需要3个按键输入、2个状态LED、1个数码管可能需要4-8个IO五人模式再加2个按键和1位数码管用MSP430G2553这种20引脚芯片就能搞定不需要外扩。第三MSP430内置的时钟系统比较灵活可以使用内部DCO省掉外部晶振电路更简单。虽然精度不如晶振但表决器对时间精度要求很低内部时钟完全够用。第四也是比较现实的一点很多高校的嵌入式课程已经引入了MSP430学生接触过之后再用它做课程设计比从一开始学新平台要顺畅。而且TI的Code Composer Studio免费开发成本为零。1.3 整体方案选型对比我考虑过几种方案简单做了对比方案优势劣势结论纯逻辑芯片搭建无需编程响应快电路复杂不易扩展人数不推荐51单片机资料多入门简单功耗高IO口数量紧张可做但不够出彩STM32资源丰富性能强成本偏高杀鸡用牛刀不推荐MSP430低功耗IO够用成本低资料相对少推荐选型这件事没有绝对答案但在这个项目里MSP430是最平衡的选择。后面所有的设计都基于MSP430G2553这颗芯片展开如果你用的是MSP430F149或者其他型号思路是通用的改一下引脚定义就行。2. 硬件电路设计要点2.1 最小系统与IO口分配MSP430G2553的最小系统非常简单电源、复位、下载接口、时钟就四件事。电源我用的是3.3V稳压输出因为MSP430G2553的工作电压范围是1.8V到3.6V3.3V是最稳妥的。复位脚RST/NMI接一个10kΩ上拉电阻到VCC再接一个0.1μF电容到地这样能防止外部干扰导致复位。下载接口用MSP430常见的2线SBW接口只需要连接TEST和RST两个引脚。我用的是MSP-EXP430G2 LaunchPad自带的仿真器直接通过排针引出SBW信号线烧录调试都很方便。IO口分配按照功能来做三人模式和五人模式共用大部分引脚只是多出来的按键接到预留IO口上。我用的分配方案如下功能引脚说明按键1-3P1.0-P1.2三人模式使用输入带上拉按键4-5P1.3-P1.4五人模式扩展用默认释放主持人锁定键P1.5投票结束后按下锁定结果复位键P2.0重新开始投票绿灯P2.1通过时点亮红灯P2.2不通过时点亮数码管段选P1.6-P2.7等共阴极数码管显示同意票数蜂鸣器P2.3锁定结果时短鸣提示注意P1.3和P1.4在LaunchPad板上连接了LED如果你用的是开发板需要把跳线帽拔掉否则按键输入会和板载LED冲突。2.2 按键电路与上下拉电阻的选择按键处理是这个项目里最容易翻车的地方。我用的按键是四脚贴片轻触开关机械结构决定了按下和释放瞬间会产生大约5到20毫秒的抖动。如果直接读IO口电平一次按键会被识别成好几次所以硬件上要加RC滤波或者软件上做消抖或者两者结合。我的做法是每个按键一端接地另一端接IO口同时在IO口和VCC之间接一个10kΩ上拉电阻。这样按键未按下时IO口读到高电平按下时IO口被拉低读到低电平。为什么用上拉而不用下拉因为MSP430内部虽然有上拉电阻但内部上拉的阻值一般比较大约20kΩ到50kΩ抗干扰能力不如外部10kΩ电阻。而且在按键接GND的情况下默认电平为高程序逻辑上判断低电平为有效这样即使IO口悬空也不会误触发。如果你用的是MSP430G2553可以省掉外部上拉电阻直接配置PxREN寄存器和PxOUT寄存器来使能内部上拉。但我的建议是实验板上还是加上外部电阻更稳定尤其是当按键线比较长的时候。2.3 显示和提示电路设计表决结果需要直观显示。我用了一个共阴极数码管显示同意票数两个LED分别表示通过和不通过再加一个蜂鸣器做声音反馈。数码管共有7个段加1个小数点共8个引脚。如果用端口直接驱动占用IO口太多。我用了动态扫描方式把所有段选线并联分别接到P1.6、P2.4、P2.5、P2.6、P2.7等引脚位选线单独接一个IO口。这样显示三位数只需要8根段选线加3根位选线比静态显示省IO。但是考虑到MSP430G2553总共也就20个引脚如果既要按键又要数码管IO口会非常紧张。所以我最后用的是更简单的方案用两个7段数码管一共需要14个段选引脚显然不够。于是我只用了一个数码管显示数字0到9因为同意票数最多也就5票个位数足够显示。数码管采用共阴极结构公共端接地段选引脚输出高电平点亮对应段。要用三极管驱动位选还是直接驱动得算一下电流。MSP430的IO口最大输出电流大约6mA左右一个LED段的工作电流通常在5到10mA如果直接驱动多个段单片机会承受不了。所以我加了一个74HC573锁存器做段驱动用两个IO口控制锁存和数据。如果你手头没有74HC573也可以用ULN2003达林顿管驱动或者干脆用LCD1602显示。LCD1602的好处是能显示文字和数字接线也只需要8根数据线加2根控制线。不过在这个项目中一个数码管足够直观成本也更低。3. 核心程序实现与逻辑拆解3.1 GPIO初始化与时钟配置MSP430的软件开发环境我用的CCS编译器自带头文件msp430g2553.h。程序结构主要分三块GPIO初始化、主循环扫描按键、投票状态判断。初始化代码要做的第一件事是配置时钟。MSP430G2553默认使用DCO频率大约1MHz。如果不想配置时钟直接用默认值也行但建议显式设置一下避免不同芯片之间的差异。我设置DCO频率为1MHz这样延时函数的计时比较准。GPIO初始化包括按键输入口、LED输出口、蜂鸣器输出口和数码管控制口。初始化时还要配置按键IO的内部上拉或者使用外部上拉电阻。#include msp430.h #define KEY1 BIT0 // P1.0 按键1 #define KEY2 BIT1 // P1.1 按键2 #define KEY3 BIT2 // P1.2 按键3 #define KEY4 BIT3 // P1.3 按键4五人模式扩展 #define KEY5 BIT4 // P1.4 按键5五人模式扩展 #define KEY_LOCK BIT5 // P1.5 主持人锁定键 #define KEY_RESET BIT0 // P2.0 复位键 #define LED_GREEN BIT1 // P2.1 绿灯 #define LED_RED BIT2 // P2.2 红灯 #define BUZZER BIT3 // P2.3 蜂鸣器 void init_io(void) { // 关闭看门狗 WDTCTL WDTPW | WDTHOLD; // 设置时钟 DCO 为 1MHz BCSCTL1 CALBC1_1MHZ; DCOCTL CALDCO_1MHZ; // 按键输入P1.0-P1.5设置为输入 P1DIR ~(KEY1 | KEY2 | KEY3 | KEY4 | KEY5 | KEY_LOCK); // 使能内部上拉让未按下时电平为高 P1REN | (KEY1 | KEY2 | KEY3 | KEY4 | KEY5 | KEY_LOCK); P1OUT | (KEY1 | KEY2 | KEY3 | KEY4 | KEY5 | KEY_LOCK); // P2.0复位键输入同样上拉 P2DIR ~KEY_RESET; P2REN | KEY_RESET; P2OUT | KEY_RESET; // LED和蜂鸣器输出 P2DIR | LED_GREEN | LED_RED | BUZZER; P2OUT ~(LED_GREEN | LED_RED | BUZZER); // 数码管控制引脚输出 // 这里简化为用P1.6做数据P1.7做锁存 P1DIR | BIT6 | BIT7; P1OUT ~(BIT6 | BIT7); }这里我特别设置了P1.0到P1.5的上拉电阻。如果你在硬件上已经加了外部10kΩ上拉内部上拉可以不用打开两者的效果是一样的。不过内部上拉的好处是省元件坏处是阻值偏大抗干扰能力稍弱。实验环境用内部上拉就够了现场演示或者做产品还是加外部电阻更可靠。3.2 按键消抖的两种实现方式按键消抖是表决器必须处理的问题。我之前见过有些同学不加消抖导致按一次键数码管跳了好几个数字整个表决过程没法看。消抖本质上是在电平变化后等一段时间确认电平稳定了再认定是有效按键。第一种方式是延时消抖也是最简单的方法。检测到IO口电平变化后延时20毫秒再读一次电平如果还是变化后的电平就认定有效。20毫秒这个数值怎么来的机械按键的抖动时间通常在5到10毫秒之间极端情况下可能到20毫秒所以延时20毫秒是经验值能覆盖绝大多数按键。第二种方式是定时器扫描消抖利用定时器中断每5毫秒扫描一次按键电平连续多次读到稳定电平才判断有效。这种方式不阻塞CPU但代码复杂度高一些。我采用的是第一种因为表决器对按键响应速度要求不高20毫秒延时完全在可接受范围内。实现代码如下unsigned char debounce_read(unsigned char pin, unsigned char port) { unsigned char state; // 第一次读取 if (port 1) { state (P1IN pin) ? 0 : 1; } else { state (P2IN pin) ? 0 : 1; } if (state) { // 延时20ms再确认 __delay_cycles(20000); if (port 1) { if ((P1IN pin) 0) return 1; } else { if ((P2IN pin) 0) return 1; } } return 0; }这段代码的核心思想是如果第一次读到低电平按键按下等20毫秒后再读一次如果还是低电平说明确实按下了返回1否则当作抖动忽略。3.3 投票状态机设计表决器不能只是简单读按键然后判断否则会出现一个问题一个人按了同意之后他松开再按一次票数会增加。必须设计一个状态机让投票过程规范化。我把过程分成四个状态空闲态、投票态、锁定态、显示保持态。空闲态等待主持人按下开始键或者系统上电后自动进入投票态。投票态参与人按键有效每按一次只记一票重复按不重复计票。锁定态主持人按下锁定键后禁止继续投票开始统计结果。显示保持态显示统计结果等待复位键清除重来。每个参与者的票数不是靠计数累加而是靠按键标志位实现。比如三人模式下定义三个标志位vote1、vote2、vote3初始为0。某人按下按键对应标志位置1。因为标志位只有0和1两个状态所以无论按多少次最终只算一票。这正好模拟了真实投票场景每个人要么投同意要么不投。状态机代码如下enum { STATE_IDLE, STATE_VOTING, STATE_LOCKING, STATE_DISPLAY }; unsigned char state STATE_IDLE; unsigned char vote1 0, vote2 0, vote3 0, vote4 0, vote5 0; unsigned char mode_5 1; // 1表示五人模式0表示三人模式这里我把模式选择做成一个宏实际工程里也可以用拨码开关来切换程序运行时读取一个IO口电平判断。3.4 核心投票判定三人与五人的区别投票判定是整个程序的核心算法极其简单但容易写错的地方在于“多数”的边界条件。三人表决器总人数是3同意票数大于等于2就算通过。即同意票数必须超过一半但一半是1.5所以向下取整是2。五人表决器总人数是5一半是2.5向下取整是3。所以边界条件就是同意票数大于总人数的一半用整数运算表示就是同意票数乘以2大于总人数。unsigned char is_pass(unsigned char total, unsigned char agree) { return (agree * 2 total); }这个写法比直接写agree total / 2 1更通用因为total是奇数时整除会出问题。比如total5时total/22需要加1才能得到3写起来容易漏。统计同意人数时直接把各投票标志位累加unsigned char total_votes(void) { unsigned char total 0; if (vote1) total; if (vote2) total; if (vote3) total; if (mode_5) { if (vote4) total; if (vote5) total; } return total; }如果是三人模式就只统计前三个人五人模式则全部统计。这个设计让一套代码同时支持两种模式切换只需要改变mode_5的值。3.5 显示和声光提示的驱动逻辑当主持人按下锁定键时系统进入锁定态此时要一次性完成判定并显示结果。我用的数码管是共阴极数字0到9对应的段码如下unsigned char seg_code[10] { 0x3F, // 0 0x06, // 1 0x5B, // 2 0x4F, // 3 0x66, // 4 0x6D, // 5 0x7D, // 6 0x07, // 7 0x7F, // 8 0x6F // 9 };显示函数的核心是把总票数拆成个位和十位然后分别点亮对应的数码管位。由于数码管是动态扫描需要在主循环里反复刷新两个位否则会出现亮度不均或者闪烁。void display_number(unsigned char num) { unsigned char tens num / 10; unsigned char ones num % 10; // 显示十位 P1OUT | BIT7; // 锁存关闭 // 这里通过串行方式将段码数据送入74HC573 // 简化起见用一个函数发送段码 send_seg(seg_code[tens]); P1OUT ~BIT7; // 锁存打开数据锁存 // 点亮十位位选 // 显示个位同理 }实际工程里动态扫描必须有延时否则切换太快人眼分辨不出来。我一般每位数码管显示2毫秒然后切换下一位。三个位走一轮是6毫秒刷新率大约166Hz肉眼看很稳定。蜂鸣器我在锁定结果时让短鸣一次。发声的原理是给蜂鸣器引脚输出一个一定频率的方波持续100毫秒左右。无源蜂鸣器需要提供频率信号有源蜂鸣器只要给高电平就能响。我用的无源蜂鸣器代码如下void buzzer_beep(unsigned int duration_ms, unsigned int freq_hz) { unsigned long cycles (unsigned long)duration_ms * freq_hz / 1000; unsigned int i; for (i 0; i cycles; i) { P2OUT | BUZZER; __delay_cycles(500000 / freq_hz); P2OUT ~BUZZER; __delay_cycles(500000 / freq_hz); } }3.6 五人模式的代码扩展思路从三人切换到五人听起来像是改一个循环次数的问题实际上还需要考虑按键数量和显示数量。我之前说过三人模式只需要三个按键、一个数码管就能显示0到3。五人模式则需要五个按键显示数字最多到5一个数码管依然够用。所以硬件上的改动就是多接两个按键软件上的改动就是多定义两个标志位。但这还不够投票状态机也要做相应调整。比如在投票态需要循环扫描五个按键在锁定态要统计五个人的票数。我把按键扫描部分独立成函数根据mode_5的值决定扫描几个按键这样逻辑清晰也方便后续扩展更多人数。void scan_keys(void) { if (debounce_read(KEY1, 1)) vote1 1; if (debounce_read(KEY2, 1)) vote2 1; if (debounce_read(KEY3, 1)) vote3 1; if (mode_5) { if (debounce_read(KEY4, 1)) vote4 1; if (debounce_read(KEY5, 1)) vote5 1; } }有人可能会问如果几个人同时按键怎么办比如三人中的两个人几乎同时按下单片机扫描是顺序的先扫到谁就记录谁另一个稍微迟一点也会被扫到最终两人的票都能被记录。真正的并发问题出现在两个人按下时间差非常短小于一个扫描周期时不过对于人手动按键来说很难出现这种极端情况可以忽略。4. 常见问题与排查技巧实录4.1 按键抖动导致的重复计数这是我调试过程中遇到的第一个问题。刚开始我没有做软件消抖只靠硬件上拉电阻结果按下一次按键数码管上的票数直接从0跳到3。原因是机械触点接触瞬间产生了多次脉冲单片机把这当成了3次有效按下。解决方式就是前面说的20毫秒延时消抖。曾经有同学问我为什么不用电容RC滤波替代我的回答是两者可以结合使用但RC电容参数选不好会引入更长的充放电时间按键响应变慢而且增加硬件复杂度。软件消抖已经足够稳定没必要自找麻烦。4.2 内部上拉电阻不生效MSP430G2553的P1口可以通过P1REN使能内部上拉/下拉但很多人会忘记同时配置P1OUT的方向。在MSP430中内部上拉或下拉的方向由PxOUT控制PxOUT对应位为1时是上拉为0时是下拉。如果你只设置了P1REN没有设置P1OUT那么内部电阻的方向是不确定的读到的电平就会飘。正确做法是同时设置P1REN和P1OUT让IO处于“输入上拉”状态。这个坑特别隐蔽因为代码编译不报错功能却时好时坏网上搜索也很少有人提到。4.3 动态扫描数码管闪烁数码管显示闪烁通常是扫描周期太长或者延时不准导致的。比如延时时间不是刚好2毫秒而是写成了__delay_cycles(2000000)在1MHz时钟下就是2秒刷新率低到肉眼明显看到跳动。解决办法是计算一下MSP430G2553默认时钟1MHz时一个机器周期是1微秒。我要延时2毫秒就应该用__delay_cycles(2000)而不是2000000。单位换算错了会导致时间差了1000倍闪烁非常明显。另外动态扫描时最好在切换位选之前先把段码数据全部更新到位再切换位选。不然会出现“残影”也就是上一位的数字还带着余辉下一位的数字已经进来了两个数字重叠在一起看起来像乱码。4.4 蜂鸣器干扰系统复位调试时遇到过一个问题蜂鸣器一响整个系统就复位。排查了很久发现是由于蜂鸣器工作电流比较大而我的电源用的是LaunchPad板载的3.3V稳压器瞬间电流超过稳压器输出能力导致电压跌落MSP430进入欠压复位。解决方法是把蜂鸣器改为低电平驱动并且串一个限流电阻降低工作电流。或者用三极管驱动蜂鸣器IO口只负责控制三极管开关电源从外部取。这个坑在做电机驱动、继电器控制时也常见本质是驱动过大电流负载时不能直接依赖MCU的IO口输出电流。4.5 常见问题速查表为了方便后来人我把调试过程中遇到的典型问题和对应方案整理成一张表现象可能原因解决办法按键按一次票数加多次未消抖增加20ms延时消抖IO口电平不稳定内部上拉未配好同时配置PxREN和PxOUT数码管显示乱码段码表错误或位选切换太快检查段码降低刷新率数码管闪烁扫描周期过长每位数码管延时2ms蜂鸣器响时系统复位驱动电流过大导致电压跌落用三极管驱动限流按键无响应引脚被板载外设占用拔掉跳线帽或者换引脚三人模式正常五人模式按键无效模式选择没生效检查mode_5标志和接线5. 项目落地后的几点体会这个表决器做完之后我最大的感受是硬件设计再简单软件状态机设计才是成败关键。以前很多人做的就是读按键、亮灯整个过程没有任何状态管理结果是逻辑混乱演示时经常翻车。我加入状态机之后整个流程变得非常清晰开始投票、参与人按键、主持人锁定、显示结果、复位重来。每个人都知道什么时候该做什么不会再出现非法操作。还有个体会是MSP430的低功耗特性在这个项目里没有完全发挥出来因为表决器大部分时间其实是待机的而我在待机时并没有进入LPM3或者LPM4模式只是空转循环功耗大概在1毫安左右。如果要做成电池供电的便携设备完全可以把显示、扫描都做成中断唤醒待机电流降到几微安。这个可以作为下一步优化的方向。如果你也想做一个类似的东西我建议先别急着焊板子先在LaunchPad上用杜邦线把原型搭出来跑通程序再画PCB。我就是这么干的结果发现数码管引脚分配有问题杜邦线改起来非常快要是已经画了板子就得重新打样了。项目做完了不代表结束还能继续扩展。比如增加无线的投票接收端让每个投票人拿一个遥控器而不是挤在一台设备前或者用OLED显示投票结果曲线再或者把数据通过串口发给上位机做成一个实时统计系统。这些玩法都基于同一个核心逻辑改动的只是外设和通讯部分。希望这篇博文能帮到你有问题欢迎在评论区交流。
返回列表