S3C2440 Linux驱动开发实战:从A/D到NandFlash全解析
简介面向 ARM 架构与 Linux 系统结合的嵌入式开发场景这份软件需求规格说明书为研发团队提供了从立项到测试的完整需求框架。文档以 BeagleBone、Raspberry Pi 等典型板卡为运行环境围绕系统管理、应用程序接口、安全性、性能优化四大模块展开并细化系统启动、设备驱动、内存管理、POSIX 接口、TCP/IP 网络编程等具体需求同时给出内外部接口划分与性能、可维护性指标适合嵌入式软件工程师、项目经理及测试人员作为需求评审和开发依据。包体为单个 docx 文档约 116KB共 1 个文件内容采用标准规格书结构包含引言、运行环境、功能需求、接口需求、性能需求、运行需求等章节目录清晰、便于直接检索。已有 194 人浏览学习对需要规范编写或快速理解 ARMLinux 平台需求文档的读者具有一定参考价值。1. 一份 S3C2440 Linux 2.6.32 规格书外协开发应该先看哪里拿到《ArmLinux 开发平台软件需求规格说明书》这类文档第一反应往往是翻功能需求其实对做外协开发的团队来说最有价值的是运行环境和接口定义这两章。这份文档描述的硬件是 S3C2440400MHz 主频、64MB 内存、256MB NandFlash软件栈是 Uboot Linux-2.6.32.2 Yaffs2。这个组合在 2011 年前后是工业控制领域的常见配置放到今天看虽然老但驱动框架和接口逻辑依然可以作为教学和改造的底稿。文档最大的特点是接口划分清楚A/D 采集、外围通信、数据存储、界面显示四条线互不纠缠。对外协方来说看这种文档的要点不是逐字读需求描述而是先把硬件资源边界和内核版本对应的驱动模型确认好再决定哪些模块直接复用内核已有驱动哪些必须自己写。下面我按这份规格书的内容把关键模块拆开讲并给出能直接落地的代码写法。2. A/D 采集驱动怎么从需求落地成 file_operations 代码2.1 需求里的采集链路到底长什么样文档里的硬件链路是模拟信号进入 FPGA 高速采集卡FPGA 做前端处理S3C2440 通过 CPCI 接口与 FPGA 通信。FPGA 负责把 -5V 到 5V 的模拟电平以 200KSPS 到 1MSPS 的采样率送入 A/D 转换电路A/D 转换器完成量化和编码后S3C2440 把数字量读上来交给上层应用。这里要注意S3C2440 本身有自带的 8 路 10 位 ADC但文档里用 CPCI 外扩 FPGA 采集卡说明对这个平台来说采集精度和采样率的要求超出了片上 ADC 的能力。驱动开发的边界是S3C2440 侧只需要把 FPGA 采集卡映射为字符设备对外提供 open、read、ioctl 三个操作即可内部逻辑按顺序调用 adc_enable()、adc_read()、adc_disable() 完成一次采样。2.2 file_operations 结构体怎么填才完整文档给出的是旧版内核 devfs 时代的驱动写法函数声明是 static int adc_enable(int ch)、static int adc_disbale()、static int adc_read(int ch)ioctl 函数的签名也还是 2.6.32 之前的格式ssize_t adc_ioctl(struct inode* inode, struct file* file, unsigned int cmd, unsigned long arg);在 Linux-2.6.32 里内核已经不再推荐用户直接调用 devfs_register_chardev()而是用 register_chrdev() 或者 cdev_add()。但规格书既然写了 devfs 接口外协实现时需要考虑兼容性。我一般会按旧接口写一版再用平台设备模型包一层这样在 2.6.32 上能跑后续升级到 3.x 内核也有迁移路径。file_operations 建议这样组织static struct file_operations adc_ops { .owner THIS_MODULE, .read adc_read, .write adc_write, .ioctl adc_ioctl, .open adc_open, .release adc_release, };其中 ioctl 里用 switch 区分命令字命令字建议用 _IO/_IOR/_IOW 宏生成这样用户态和内核态能保持一致的编码规则。很多项目直接在 ioctl 里写死数字 0、1、2短时间没问题长期维护非常痛苦。用标准宏的好处是用户态可以通过 ioctl(fd, ADC_READ_CH, ch) 这样的调用直接传参不用自己拼装结构体。2.3 模块加载卸载insmod 之后要能干净撤掉模块加载和卸载函数看起来简单但最容易漏的是错误处理。文档里给出了加载函数内部调用初始化函数、卸载函数内部调用 devfs_unregister_chrdev() 和 devfs_unregister() 的框架实际写的时候要注意顺序static int __init AD_init(void) { int ret; ret HW_AD_init(); /* 注册字符设备、获取操作句柄 */ if (ret 0) { printk(KERN_ERR AD init failed: %d\n, ret); return ret; } printk(KERN_INFO AD driver loaded\n); return 0; } static void __exit cleanup_AD(void) { devfs_unregister_chrdev(AD_MAJOR, DEVICE_NAME); devfs_unregister(ad_dev_handle); printk(KERN_INFO AD driver unloaded\n); } module_init(AD_init); module_exit(cleanup_AD); MODULE_LICENSE(GPL);加载失败时要把前面已经申请的资源全部释放否则第二次 insmod 会报设备号冲突或内存泄漏。这里有个细节devfs_register() 返回的操作句柄要保存为全局变量卸载时按注册的反序释放先注销设备节点再注销字符设备。2.4 用户态拿数据的姿势read 还是 ioctl文档提到上层应用接收到 A/D 转换结果后要做后续处理但没有说应用层怎么调用。常见做法是两种一是用 read() 直接读驱动在 read 里完成一次 adc_enable adc_read adc_disable 流程把结果返回给用户缓冲区二是用 ioctl() 指定通道后 read()。推荐第二种因为多通道采集时用户态需要明确知道当前读的是哪个通道int ch 2; int value; ioctl(fd, ADC_SET_CHANNEL, ch); read(fd, value, sizeof(value));这个模式的好处是驱动内部可以把 adc_enable 和 adc_disable 之间的时序控制封装好应用层不用关心采样保持时间、转换启动延迟这些参数。FPGA 侧如果支持连续采样模式read 里还能直接做环形缓冲批量读取吞吐量更高。3. UART、网口、USB 驱动要点与串口 select 读写实测写法3.1 三类串口并存内核注册方式要分清文档要求 2 路 RS232 和 1 路 RS422/RS485波特率 2400bps 到 115200bps 可设。这里第一个坑是S3C2440 原生 UART 只有 3 路文档规划正好全占满一路调试、一路通信、一路 RS485没有余量。RS485 的收发切换是个经典问题。文档明确写了 RS485 用差分线传输逻辑“1”对应 2V 到 6V 电压差逻辑“0”对应 -2V 到 -6V。实际驱动实现时如果用的是自动收发切换芯片驱动按普通 UART 处理即可如果是软件控制方向就需要在每次 write 前把 GPIO 拉高进入发送模式发送完成后再拉低。我一般建议在 uart_ops 的 start_tx 和 stop_tx 回调里完成这个切换而不是放在应用层避免两个进程同时操作串口时方向控制打架。串口驱动注册的关键数据结构是 uart_port文档强调了注册串口驱动就是注册 uart_port 数据结构。实际操作时还要实现 struct uart_ops 里的所有回调函数包括 start_tx、stop_tx、start_rx、tx_empty、set_termios 等十几个接口缺一个编译能过但运行时会出诡异问题。3.2 串口应用层编程select 超时处理必须写完整文档给出了串口读写的主函数框架open 后调用 init_ttyS(fd) 初始化和 rs232_transfer(fd) 通信。其中 init_ttyS 要完成清空原有设置、设置波特率、设置标志位三个动作这个用 tcgetattr 和 tcsetattr 配合 termios 结构体实现。通信函数里的 select 写法是嵌入式串口通信的标准范式但文档代码有一个容易踩的坑FD_SET( ports[portNo].handle, rfds) 之前必须重新 FD_ZERO因为 select 返回后 rfds 会被内核修改不重置的话下一次 select 可能立即返回但实际没有数据。完整的写法应该是struct timeval tv; fd_set rfds; int retval; int actualRead; while (1) { FD_ZERO(rfds); FD_SET(ports[portNo].handle, rfds); tv.tv_sec Timeout / 1000; tv.tv_usec (Timeout % 1000) * 1000; retval select(ports[portNo].handle 1, rfds, NULL, NULL, tv); if (retval -1) { printf(select error!\n); break; } else if (retval) { actualRead read(ports[portNo].handle, buff, maxCnt); /* 处理 actualRead 为 0 的情况可能是设备端断连 */ } }select 的第一个参数是最大文件描述符加 1文档代码里写的是 ports[portNo].handle 1这里要注意是句柄值加 1不是端口号加 1。timeout 参数设计也很关键如果 Timeout 是毫秒tv_usec 需要除以 1000 换算成微秒文档代码写的是 Timeout % 1000 * 1000这个运算顺序等价于 (Timeout % 1000) * 1000是可以的但可读性差建议拆行写。3.3 网口驱动sk_buff 贯穿三层初始化到底做了几件事网络驱动部分文档给出了五层结构图网络协议接口层、网络设备接口层、设备驱动功能层、网络媒介层和设备管理层。核心数据结构是 net_device 和 sk_buff。初始化流程文档写得很清楚检测物理设备、资源配置、构造 device 数据结构、register_netdevice() 注册。这个过程落到代码里就是 net_probe() 到 register_netdev() 的路径。S3C2440 平台如果外接 RTL8201BL 做 PHYMAC 在 CPU 内部驱动需要做的事情是初始化 MAC 控制器、配置 PHY 的寄存器、建立 MII 接口通信。数据包接收的中断处理是网卡驱动最容易出性能问题的地方。文档提到从硬件接收缓冲区读取数据组合成 sk_buff 结构再通过 netif_rx() 交给上层。这里有个分配 sk_buff 的优化点使用 dev_alloc_skb() 分配而不是普通 kmalloc因为 dev_alloc_skb 会考虑缓存对齐和 DMA 一致性避免 cache 一致性问题。环回测试的方法是 ifconfig eth0 up 后 ping 本机 IP或者直接用 ping -I eth0 网关地址。更深入一点的验证是在驱动里加计数每次中断和每次 netif_rx 调用都累加对比 /proc/interrupts 的中断次数能确认数据包是否真的经过了驱动。3.4 USB 驱动分层到底是写 host 端还是 device 端文档里 USB 需求分三种角色2 路 host 用于连接鼠标和 U 盘1 路从机用于调试。对驱动开发来说工作量最大的是 U 盘驱动因为 USB mass storage 协议栈虽然有内核实现但 S3C2440 这类芯片的 USB host 控制器驱动往往不完善。文档给出的 USB 主机端驱动框架六个步骤以 usb_register() 开始以 usb_deregister() 结束。中间的 probe 回调是重点probe 函数负责分配内存和设备请求。实际运行时鼠标会走 HID 协议内核的 usbhid 驱动可以直接复用U 盘走 usb-storage 驱动关键是确认 usb_stor_scan 能正确枚举设备。设备端驱动文档给了五个模块初始化、数据传输、设备请求、厂商请求、其他操作。如果从机模式是用来调试的建议直接用内核的 gadget 框架配置为串口或网卡功能这样宿主机不用装额外驱动就能通信。手动实现设备端驱动要处理端点描述符和配置描述符的排列问题工作量大且容易出错。4. NandFlash 分区、Yaffs2 与开机即插即用的运行需求4.1 256MB 空间怎么分配才够用文档规定 NandFlash 共 256MB地址范围 0x0 到 0x0fffffff前 32MB 存放 Linux 内核镜像和配置文件。这个分区规划在 2011 年很常见但今天来看偏紧张一个完整的 rootfs 加上应用程序很容易超过 32MB。常见的分区策略是按 Uboot 的 mtdparts 参数分裂比如分区名起始地址大小用途bootloader0x000000512KBUbootkernel0x0800004MBLinux 内核镜像rootfs0x400000剩余空间Yaffs2 根文件系统app自定义预留应用程序和数据文档说前 32MB 用于内核镜像和配置文件这个表述有点模糊。建议明细到 Uboot、内核、dtb、启动参数四个区域否则外协方很容易把分区表写死后期调整很痛苦。4.2 Yaffs2 对 NandFlash 的坏块处理逻辑文档指定文件系统用 Yaffs2这是专门为 NandFlash 设计的日志型文件系统特点是支持掉电安全、磨损均衡和坏块管理。S3C2440 的 NandFlash 控制器支持硬件 ECC但 Yaffs2 默认用软件 ECC二者选一不能混用。运行时的坑主要出现在文件系统写满或者掉电场景。Yaffs2 会扫描整个 Flash 重建目录结构坏块越多扫描越慢。如果 rootfs 分区在 Yaffs2 挂载后反复断电可能出现 chunk 分配错误表现为某个文件读出来是乱码。我一般会在应用层增加一个文件系统健康检查定期用 fsck.yaffs2 排查但要注意 Yaffs2 的 fsck 工具在老内核版本上不活跃更可靠的方式是监控空闲块数量低于阈值提前告警。4.3 开机界面和即插即用到底属于需求还是实现文档把开机界面、应用程序界面、即插即用都归入了运行需求。从外协开发的角度这些内容描述得太粗真正实现时会遇到一个关键问题7 寸 LVDS 屏 800x480 分辨率的 framebuffer 初始化以及开机 logo 的显示时机。即插即用部分文档要求网口、串口、USB 口都能热插拔。USB 口的热插拔在 2.6.32 内核里已经原生支持网口和串口在嵌入式平台上其实是设备固定存在的所谓即插即用更多是指驱动模块可以动态加载卸载而不是物理接口可以带电插拔。实现层面要注意的是 hotplug 脚本链。文档给出了 /sbin/hotplug 到 /etc/hotplug/*.agent 的调用链路这个机制从 2.6.15 内核以后逐渐被 udev 取代。如果平台用 2.6.32可以确认一下内核是否配置了 CONFIG_UEVENT_HELPER 和 udev如果没有配置USB 设备插上后设备节点可能不会自动创建需要手动 mknod。4.4 文件系统与驱动的联动问题Yaffs2 挂载后应用层对 U 盘的操作通过 vfat 文件系统完成内核需要同时配置 CONFIG_VFAT_FS 和 CONFIG_NLS_UTF8否则 U 盘里的中文文件名会显示为乱码。这和 NandFlash 上的 Yaffs2 没有直接关系但属于同一个存储子系统的完整链路。文档的数据存储章节没有给出 NandFlash 分区存储规划的详细表格外协开发时应主动输出一份分区表明确每个分区的起始块、结束块、文件系统类型和读写权限。尤其是 app 分区如果设计为可写建议单独使用一个分区而不是直接放大 rootfs避免应用崩溃导致整个文件系统进入只读状态。5. 验收外协驱动的几个具体检查点与性能验证方法文档的第五章性能需求只列了稳定性、实时性、可扩展性、可维护性四个标题没有给出量化指标。作为甲方验收时如果连指标都没有外协方交付的东西很难评判。这里给出几个可执行的验证方法覆盖文档提到的性能项。第一个检查点是驱动代码里是否还有进程上下文和中断上下文混用的问题。Linux 2.6.32 里中断处理函数中不能调用可能睡眠的函数比如 kmalloc 带 GFP_KERNEL 标志、copy_from_user、mutex_lock 等。验收时 grep 驱动源码看中断处理函数里有没有这些调用如果有直接判不合格。第二个检查点是用 top 或 /proc/slabinfo 观察驱动长期运行后的内存增长。S3C2440 内存只有 64MB驱动一旦有内存泄漏系统跑几个小时就会因为内存不足触发 OOM。验证方法是反复开关设备节点一千次对比 cat /proc/meminfo 的 MemFree 值变化超过 5% 的差异就要排查。第三个检查点是 A/D 采集的实时性。文档写采样率 200KSPS 到 1MSPS应用层如果按每次 read 读一个采样点的方式拿数据系统调用开销会大得离谱。1MSPS 意味着 1 微秒一个采样点而一次 read 系统调用本身就要几十微秒。正确的验证方式是驱动侧用 DMA 配合环形缓冲批量搬运数据应用层 read 一次拿到一批数据后再解析。验收时给一个频率已知的正弦波信号输入连续采集后做 FFT看频谱上是否有明显的谐波失真和底噪抬升。第四个检查点是串口和网口的长时间稳定性。RS485 接口按文档要求在 2400bps 到 115200bps 之间可设验收时用 115200bps 连续跑 24 小时统计丢字节数和误码率。以太网口用 iperf 打流确认 100M 全双工模式下吞吐量能跑满。如果用的是 RTL8201BL还要确认网线拔出重插后协商速率能正确恢复这个和 PHY 驱动的中断处理有直接关系。最后一个检查点是按键矩阵的扫描算法。文档要求 20 个以上按键采用矩阵接法软件扫描防抖。验收时可以连续快速按不同按键组合观察是否出现丢键或串键。环形队列缓冲区长度文档建议 10 个按键代码如果应用层消费速度跟不上下发速度队列溢出是必然的。这时候要么加大队列深度要么在驱动里加背压让应用层通过 select 感知缓冲区状态再读取而不是无脑轮询。本文还有配套的精品资源点击获取