☰
C# RFID智能公交刷卡系统:串口通信与数据库扣费实战
2026/10/31 13:43:13 网站建设 项目流程

简介:这份资源是一套面向C#初学者与物联网开发入门者的完整项目源码,以智能公交刷卡系统为场景,演示如何用C#对接RFID读卡器完成刷卡乘车、身份验证与扣费流程。项目覆盖串口通信、事件驱动编程、ADO.NET数据库操作、WinForms界面设计、异常处理与多线程等知识点,适合作为课程设计或毕业设计的参考实例。压缩包共57个文件,约465KB,包含6个cs源码文件、6个exe可执行程序、6个pdb调试符号、2个mdb数据库、2个dll动态库,以及sln解决方案、csproj工程文件、resx资源文件和课程设计文档等,结构完整可直接运行调试。目前已有744人学习下载。通过阅读源码与文档,读者可掌握RFID硬件接口调用、刷卡事件响应、乘车记录存储与费用结算的实现思路,并借鉴其异常捕获与异步处理方式,快速搭建自己的物联网刷卡应用原型。

1. 从一张刷不上的公交卡说起:这套 C# RFID 刷卡系统到底能跑通什么

早高峰,乘客把卡片贴到读卡器上,蜂鸣器没响,司机喊了一句“再靠一下”,后面排队的人开始躁动。这个场景在公交刷卡系统里太常见了,问题可能出在卡片、读卡器、串口通信、扣费逻辑或者数据库连接中的任何一环。我拿到这份「C#实现基于RFID的智能公交刷卡系统.zip」的时候,第一反应不是看界面长什么样,而是想知道它到底把哪几段链路跑通了——是只做了个模拟刷卡动画,还是真的能从串口读到卡号、查余额、扣费、写乘车记录。

这份资源是一个典型的 C# 上位机课程设计项目,包含exp.sln解决方案文件、RFID项目目录、exp.suo用户配置文件和一份RFID课程设计文档.doc。它模拟的是公共交通场景下的非接触式刷卡乘车流程:RFID 卡片靠近读卡器,读卡器通过串口把卡号传给 C# 程序,程序查数据库确认用户身份和余额,扣费成功后写一条乘车记录,界面刷新显示。适合正在做课程设计、想找一个能跑的 C# 上位机 + RFID 串口通信参考项目的学生,也适合刚接触 WinForms 和 ADO.NET、想看看一个完整小系统怎么把硬件交互和数据库串起来的开发者。它不涉及真实公交系统的清分结算和黑名单同步,但作为理解 RFID 刷卡闭环的起点,代码结构是完整的。

2. 拆开压缩包先看什么:解决方案结构与 RFID 串口通信链路

2.1 用 Visual Studio 打开 exp.sln 后的第一眼

解压之后不要急着双击.suo,那个文件是 Visual Studio 的用户配置缓存,换一台机器或者换一个 VS 版本经常导致加载异常。正确的入口是exp.sln。用 Visual Studio 2015 及以上版本打开,如果提示“此项目与当前 Visual Studio 版本不兼容”,先看.csproj里的TargetFrameworkVersion,常见的是.NET Framework 4.5或4.0,在项目属性里改成你本机装了的框架版本即可。

打开之后解决方案资源管理器里通常只有一个 WinForms 项目,名字可能是RFID或exp。重点看三个地方:Form1.cs或主窗体文件、一个负责串口通信的类文件、以及App.config里的数据库连接字符串。课程设计项目往往把逻辑都堆在窗体代码里,这不是好习惯,但拆起来快。先找到串口初始化的代码,一般在窗体构造函数或者Form_Load事件里。

// 典型的串口初始化代码,参数需要和读卡器实际配置一致 private SerialPort rfidPort = new SerialPort(); private void InitRfidPort() { rfidPort.PortName = "COM3"; // 读卡器实际占用的串口号 rfidPort.BaudRate = 9600; // 常见波特率,也有 115200 的模块 rfidPort.DataBits = 8; // 数据位 rfidPort.StopBits = StopBits.One; // 停止位 rfidPort.Parity = Parity.None; // 校验位 rfidPort.DataReceived += new SerialDataReceivedEventHandler(RfidPort_DataReceived); rfidPort.Open(); }

这段代码里PortName是最容易翻车的地方。设备管理器里看到的 COM 口号不一定就是读卡器,有些 USB 转串口芯片会占用 COM1 或 COM2,插拔之后口号还会变。BaudRate必须和读卡器模块的出厂设置一致,常见的 RFID 读卡模块有 9600 和 115200 两档,设错了收到的就是乱码。DataReceived事件是事件驱动编程的典型用法,读卡器有数据上来时自动触发,不需要在主线程里轮询。

2.2 读卡器数据帧的解析逻辑

RFID 读卡器通过串口吐出来的不是裸卡号,而是一帧带包头、长度、命令字、数据和校验的字节流。不同厂家的帧格式不一样,但课程设计里常见的是类似0x02开头、0x03结尾的格式,中间是 ASCII 码的卡号。解析的时候不能假设一次DataReceived就能收到完整一帧,串口数据可能分多次到达。

private List<byte> receiveBuffer = new List<byte>(); private void RfidPort_DataReceived(object sender, SerialDataReceivedEventArgs e) { int bytesToRead = rfidPort.BytesToRead; byte[] temp = new byte[bytesToRead]; rfidPort.Read(temp, 0, bytesToRead); receiveBuffer.AddRange(temp); // 检查是否收到完整帧,这里以 0x02 开头 0x03 结尾为例 while (receiveBuffer.Count >= 2) { int startIndex = receiveBuffer.IndexOf(0x02); int endIndex = receiveBuffer.IndexOf(0x03); if (startIndex >= 0 && endIndex > startIndex) { byte[] frame = receiveBuffer.GetRange(startIndex, endIndex - startIndex + 1).ToArray(); receiveBuffer.RemoveRange(0, endIndex + 1); string cardId = Encoding.ASCII.GetString(frame, 1, frame.Length - 2); // 跨线程更新 UI 必须用 Invoke this.Invoke(new Action(() => ProcessCard(cardId))); } else { break; // 帧不完整,等下一次数据 } } }

这里有几个关键点。第一,receiveBuffer用List<byte>而不是数组,因为帧长度不固定,需要动态追加。第二,IndexOf找包头包尾的时候要处理粘包和半包的情况,如果一次收到两帧,while循环能连续处理。第三,this.Invoke是 WinForms 里跨线程更新控件的标准做法,直接在DataReceived里改 Label 的 Text 会抛InvalidOperationException,这是新手最常见的翻车点之一。第四,卡号解析出来之后要Trim()掉可能的空白字符,有些读卡器会在卡号后面补空格或者回车换行。

2.3 数据库表结构和扣费事务

课程设计文档里一般会给出数据库设计,常见的是三张表:用户表存卡号、姓名、余额;乘车记录表存卡号、乘车时间、扣费金额;可能还有一张管理员表。用 SQL Server 或者 Access 的都有,连接字符串在App.config里。

// 扣费操作必须放在事务里,防止扣了钱没写记录 private bool DeductFee(string cardId, decimal fee) { string connStr = ConfigurationManager.ConnectionStrings["BusSystem"].ConnectionString; using (SqlConnection conn = new SqlConnection(connStr)) { conn.Open(); using (SqlTransaction tran = conn.BeginTransaction()) { try { // 先查余额,加 UPDLOCK 防止并发重复扣费 string checkSql = "SELECT Balance FROM UserInfo WITH (UPDLOCK) WHERE CardId = @CardId"; SqlCommand checkCmd = new SqlCommand(checkSql, conn, tran); checkCmd.Parameters.AddWithValue("@CardId", cardId); object balanceObj = checkCmd.ExecuteScalar(); if (balanceObj == null) { tran.Rollback(); return false; } decimal balance = Convert.ToDecimal(balanceObj); if (balance < fee) { tran.Rollback(); return false; } // 扣余额 string updateSql = "UPDATE UserInfo SET Balance = Balance - @Fee WHERE CardId = @CardId"; SqlCommand updateCmd = new SqlCommand(updateSql, conn, tran); updateCmd.Parameters.AddWithValue("@Fee", fee); updateCmd.Parameters.AddWithValue("@CardId", cardId); updateCmd.ExecuteNonQuery(); // 写乘车记录 string insertSql = "INSERT INTO RideRecord (CardId, RideTime, Fee) VALUES (@CardId, GETDATE(), @Fee)"; SqlCommand insertCmd = new SqlCommand(insertSql, conn, tran); insertCmd.Parameters.AddWithValue("@CardId", cardId); insertCmd.Parameters.AddWithValue("@Fee", fee); insertCmd.ExecuteNonQuery(); tran.Commit(); return true; } catch (Exception ex) { tran.Rollback(); // 记日志,不要直接 MessageBox 弹出来打断刷卡流程 File.AppendAllText("error.log", DateTime.Now + " " + ex.Message + Environment.NewLine); return false; } } } }

UPDLOCK提示是为了防止两个读卡器同时读到同一张卡导致重复扣费,虽然课程设计里一般只有一个读卡器,但加上这个习惯是好的。AddWithValue能防 SQL 注入,比字符串拼接安全。异常处理里把错误写日志而不是弹窗,是因为刷卡场景下弹窗会阻塞界面,后面的人刷不了。扣费金额一般从配置文件或者数据库里的线路票价表读取,不要硬编码在代码里。

3. 把刷卡流程跑起来:从串口调试到界面联调的完整步骤

3.1 先用串口助手确认读卡器能出数据

在跑 C# 程序之前,一定先用串口调试助手确认读卡器本身是好的。打开串口助手,选对 COM 口和波特率,把卡贴上去,看有没有数据上来。如果串口助手都收不到数据,C# 程序更不可能收到。这一步能排除掉一半的“程序读不到卡”问题。

常见的情况是:串口助手能收到数据,但 C# 程序收不到。这时候检查三个地方:COM 口有没有被串口助手占用(串口是独占资源,助手没关的话 C# 打不开);DataReceived事件有没有绑定;rfidPort.Open()有没有抛异常被吞掉。我一般会在InitRfidPort外面包一层 try-catch,把异常信息写到日志里,而不是让程序直接崩掉。

3.2 数据库连接字符串的配置和测试

App.config里的连接字符串格式取决于你用 SQL Server 还是 Access。SQL Server 常见的是Data Source=.;Initial Catalog=BusSystem;Integrated Security=True,Access 则是Provider=Microsoft.Jet.OLEDB.4.0;Data Source=|DataDirectory|\BusSystem.mdb。注意 Access 的 Provider 在 64 位系统上可能要用Microsoft.ACE.OLEDB.12.0,而且需要在项目属性里把目标平台改成 x86,否则会报“未在本地计算机上注册 Microsoft.Jet.OLEDB.4.0 提供程序”。

测试连接字符串是否有效,不用跑整个程序,写个小控制台或者用 VS 的“服务器资源管理器”连一下就行。连接通了再跑主程序,能省很多排查时间。数据库文件如果放在bin\Debug目录下,用|DataDirectory|相对路径比较稳,换机器不用改配置。

3.3 界面线程和刷卡响应的配合

WinForms 的界面线程和串口接收线程是两个线程。DataReceived事件在后台线程触发,直接操作 Label、TextBox 这些控件会抛异常。除了用Invoke,也可以用BeginInvoke异步更新,避免后台线程等界面线程。刷卡之后界面一般要做三件事:显示卡号、显示余额、显示扣费结果。如果扣费失败,用不同颜色的文字提示,不要弹 MessageBox。

private void ProcessCard(string cardId) { lblCardId.Text = "卡号:" + cardId; decimal fee = 2.0m; // 票价,实际项目里从配置读 bool success = DeductFee(cardId, fee); if (success) { lblResult.ForeColor = Color.Green; lblResult.Text = "扣费成功,扣款 " + fee + " 元"; } else { lblResult.ForeColor = Color.Red; lblResult.Text = "扣费失败,请检查卡内余额"; } // 刷新余额显示 RefreshBalance(cardId); }

票价fee不要写死在方法里,放到App.config的appSettings里,改票价不用重新编译。RefreshBalance重新查一次数据库把最新余额显示出来,让乘客看到扣费后的余额。界面刷新频率不用太高,刷卡动作本身是低频的,但要注意如果连续刷同一张卡,要有防抖逻辑,比如 2 秒内同一卡号不重复处理。

3.4 用课程设计文档对照功能清单

RFID课程设计文档.doc里一般有功能需求、数据库表结构、流程图和测试用例。跑程序之前先翻一遍文档,看看它声称实现了哪些功能,然后逐个验证。常见的功能点包括:刷卡扣费、余额查询、乘车记录查询、新卡注册、充值。课程设计项目不一定每个都做完了,文档里写了但代码里没实现的也常见。对照文档能快速知道哪些是完整链路,哪些只是界面摆样子。

如果文档里写了“支持多读卡器”,但代码里只有一个SerialPort实例,那就是没做完。如果文档里写了“数据加密”,但数据库里余额是明文,那也是没做完。把文档当需求清单用,不要当实现保证。

4. 避坑与排查:串口、数据库、线程这三处最容易翻车

4.1 串口打开失败或读不到数据

现象:程序启动时抛UnauthorizedAccessException,提示“拒绝访问 COM 口”。原因:COM 口被其他程序占用,最常见的是串口调试助手没关,或者上一次程序异常退出没释放串口。解决:关掉所有可能占用串口的程序,在设备管理器里确认读卡器对应的 COM 口,如果还是打不开,拔插一下 USB 转串口线,或者重启电脑。代码里加try-catch把异常写日志,不要让它直接崩。

4.2 卡号解析出来是乱码

现象:DataReceived收到了数据,但解析出来的卡号是乱码或者带一堆问号。原因:波特率不匹配,或者编码方式不对。读卡器输出的是 ASCII 码还是十六进制,要看模块手册。解决:先用串口助手以十六进制显示模式看原始数据,确认帧头和帧尾的字节值,再对照代码里的解析逻辑。如果是十六进制卡号,用BitConverter.ToString转成字符串,不要用Encoding.ASCII。

4.3 扣费成功但余额没变

现象:界面提示扣费成功,但重新查余额发现没扣。原因:事务提交了但更新语句没生效,或者查余额用的是另一个数据库连接没读到最新数据。解决:检查UPDATE语句的WHERE条件是否匹配到了行,ExecuteNonQuery的返回值是不是大于 0。查余额的时候不要用缓存,每次重新查数据库。如果是 Access 数据库,注意事务支持和 SQL Server 有差异,UPDLOCK提示在 Access 里不生效。

4.4 跨线程操作控件导致程序崩溃

现象:刷卡时程序突然闪退,事件查看器里能看到InvalidOperationException。原因:在DataReceived事件里直接改了控件的属性。解决:所有控件更新都通过Invoke或BeginInvoke委托到 UI 线程。如果用了async/await,注意await之后的代码可能不在 UI 线程上,更新控件前要检查InvokeRequired。

4.5 数据库文件路径在换机器后失效

现象:在自己电脑上跑得好好的,拷到同学电脑上就报“找不到数据库文件”。原因:连接字符串里用了绝对路径,或者|DataDirectory|指向的目录不对。解决:把数据库文件放在bin\Debug下,连接字符串用|DataDirectory|\BusSystem.mdb,这样不管项目拷到哪,只要bin\Debug目录结构不变就能找到。如果用的是 SQL Server,把.mdf文件附加到目标机器的 SQL Server 实例上,改连接字符串里的Data Source。

5. 进阶:把刷卡记录做成可查询的日志,以及一个防重复扣费的技巧

课程设计项目跑通之后,最值得加的一个功能是乘车记录查询。原始项目可能只写记录不查记录,但实际用起来,能按卡号和时间段查记录才算完整。在 WinForms 里加一个DataGridView,用SqlDataAdapter填充DataTable就行,注意查询条件用参数化,不要拼字符串。

private void LoadRideRecords(string cardId, DateTime from, DateTime to) { string sql = "SELECT CardId, RideTime, Fee FROM RideRecord WHERE CardId = @CardId AND RideTime BETWEEN @From AND @To ORDER BY RideTime DESC"; using (SqlConnection conn = new SqlConnection(connStr)) using (SqlDataAdapter adapter = new SqlDataAdapter(sql, conn)) { adapter.SelectCommand.Parameters.AddWithValue("@CardId", cardId); adapter.SelectCommand.Parameters.AddWithValue("@From", from); adapter.SelectCommand.Parameters.AddWithValue("@To", to); DataTable dt = new DataTable(); adapter.Fill(dt); dataGridView1.DataSource = dt; } }

BETWEEN查询的时间范围包含边界值,如果不想包含结束时刻,用>= @From AND < @To。ORDER BY RideTime DESC让最新的记录排在最上面,符合查看习惯。DataGridView的列宽可以在ColumnAdded事件里统一设置,避免每次查询都手动调。

防重复扣费除了数据库层面的UPDLOCK,在应用层也可以加一个简单的内存缓存。用一个Dictionary<string, DateTime>记录最近刷过的卡号和刷卡时间,处理卡号之前先检查距离上次刷卡是否超过 3 秒,没超过就直接忽略。这个缓存不需要持久化,程序重启就清空,够用。

private Dictionary<string, DateTime> lastSwipeTime = new Dictionary<string, DateTime>(); private readonly TimeSpan swipeInterval = TimeSpan.FromSeconds(3); private bool IsDuplicateSwipe(string cardId) { if (lastSwipeTime.ContainsKey(cardId)) { if (DateTime.Now - lastSwipeTime[cardId] < swipeInterval) return true; } lastSwipeTime[cardId] = DateTime.Now; return false; }

这个逻辑放在ProcessCard的最前面,重复刷卡直接返回,不查数据库也不扣费。swipeInterval设 3 秒是个经验值,公交场景下同一个人连续刷两次卡的概率很低,3 秒足够覆盖误触。如果读卡器灵敏度高,可以调到 5 秒。

验证整套系统是否跑通,我一般会走一遍完整流程:新卡注册、充值、刷卡扣费、查余额、查记录、再刷一次确认防重复生效。每一步都看数据库里的数据变化,不要只看界面提示。界面可以骗人,数据库不会。从那以后我每次拿到这种课程设计项目,都先把串口助手和数据库查询窗口打开,边跑边看原始数据,再去看代码逻辑。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询