| /* Copyright (c) 2010 The Regents of the University of California |
| * Barret Rhoden <brho@cs.berkeley.edu> |
| * See LICENSE for details. |
| * |
| * Rimas's Ethernet-Audio device */ |
| |
| #ifdef CONFIG_ETH_AUDIO |
| |
| #include <eth_audio.h> |
| #include <string.h> |
| #include <devfs.h> |
| #include <page_alloc.h> |
| #include <pmap.h> |
| #include <net/nic_common.h> |
| #include <process.h> |
| #include <smp.h> |
| |
| struct file_operations ethaud_in_f_op; |
| struct file_operations ethaud_out_f_op; |
| struct page_map_operations ethaud_pm_op; |
| |
| /* There is only one packet we'll ever send out. It's a full ethernet frame |
| * that we build and submit to send_frame() (which does another memcpy). */ |
| struct ethaud_udp_packet eth_udp_out; |
| |
| /* Consider a lock to protect this */ |
| struct proc *active_proc = 0; |
| |
| /* Nodes/inodes representing the device */ |
| struct inode *ethaud_in, *ethaud_out; |
| |
| /* Builds the device nodes in /dev */ |
| void eth_audio_init(void) |
| { |
| struct page *page; |
| struct file *in_f, *out_f; |
| in_f = make_device("/dev/eth_audio_in", S_IRUSR, __S_IFBLK, |
| ðaud_in_f_op); |
| out_f = make_device("/dev/eth_audio_out", S_IRUSR | S_IWUSR, __S_IFBLK, |
| ðaud_out_f_op); |
| /* Grab and save the inodes (might change if we change make_device) */ |
| ethaud_in = in_f->f_dentry->d_inode; |
| kref_get(ðaud_in->i_kref, 1); |
| ethaud_out = out_f->f_dentry->d_inode; |
| kref_get(ðaud_out->i_kref, 1); |
| /* Let go of the files (mostly used to get the dentry in the right place) */ |
| kref_put(&in_f->f_kref); |
| kref_put(&out_f->f_kref); |
| /* make sure the inode tracks the right pm op */ |
| ethaud_in->i_mapping->pm_op = ðaud_pm_op; |
| ethaud_out->i_mapping->pm_op = ðaud_pm_op; |
| /* zalloc pages and associate them with the devices' inodes */ |
| assert(!kpage_alloc(&page)); |
| memset(page2kva(page), 0, PGSIZE); |
| ethaud_in->i_fs_info = page; |
| assert(!kpage_alloc(&page)); |
| memset(page2kva(page), 0, PGSIZE); |
| ethaud_out->i_fs_info = page; |
| } |
| |
| /* Lots of unnecessary copies. For now, we need to build the ethernet frame, |
| * which means we prepend the ethernet header... */ |
| static void eth_audio_sendpacket(void *buf) |
| { |
| int retval; |
| /* Fill the outgoing buffer (Copy #1. 2 is in send_frame, 3 is the DMA). */ |
| memcpy(ð_udp_out.payload, buf, ETH_AUDIO_PAYLOAD_SZ); |
| /* Make sure there is still a reasonable header. */ |
| static_assert(sizeof(eth_udp_out) == ETH_AUDIO_FRAME_SZ); |
| /* Should compute the UDP checksum before sending the frame out. The |
| * Eth-audio device shouldn't care (and Linux seems to be okay with packets |
| * that have no checksum (but not a wrong checksum)). Technically, this |
| * hurts our performance a bit (and some NICs can offload this). */ |
| eth_udp_out.udp_hdr.checksum = 0; |
| eth_udp_out.udp_hdr.checksum = udp_checksum(ð_udp_out.ip_hdr, |
| ð_udp_out.udp_hdr); |
| /* Send it out */ |
| retval = send_frame((const char*)ð_udp_out, ETH_AUDIO_FRAME_SZ); |
| assert(retval >= 0); |
| } |
| |
| /* This is how we know who to send the packet back to, since we have no real |
| * networking stack. Lots of assumptions about how things stay in sync. */ |
| static void eth_audio_prep_response(struct ethaud_udp_packet *incoming, |
| struct ethaud_udp_packet *outgoing) |
| { |
| /* If you're looking for optimizations, we can do this just once */ |
| memcpy(&outgoing->eth_hdr.dst_mac, &incoming->eth_hdr.src_mac, 6); |
| memcpy(&outgoing->eth_hdr.src_mac, device_mac, 6); |
| outgoing->eth_hdr.eth_type = htons(IP_ETH_TYPE); |
| outgoing->ip_hdr.version = IPPROTO_IPV4; |
| outgoing->ip_hdr.hdr_len = ETH_AUDIO_IP_HDR_SZ >> 2; |
| outgoing->ip_hdr.tos = 0; |
| outgoing->ip_hdr.packet_len = htons(ETH_AUDIO_PAYLOAD_SZ + UDP_HDR_SZ + |
| ETH_AUDIO_IP_HDR_SZ); |
| outgoing->ip_hdr.id = htons(0); |
| outgoing->ip_hdr.flags_frags = htons(0); |
| outgoing->ip_hdr.ttl = DEFAULT_TTL; |
| outgoing->ip_hdr.protocol = IPPROTO_UDP; |
| /* Need a non-broadcast IP. Picking one higher than the sender's */ |
| outgoing->ip_hdr.src_addr = htonl(ntohl(incoming->ip_hdr.src_addr) + 1); |
| outgoing->ip_hdr.dst_addr = incoming->ip_hdr.src_addr; |
| /* Since the IP header is set already, we can compute the checksum. */ |
| outgoing->ip_hdr.checksum = 0; |
| outgoing->ip_hdr.checksum = ip_checksum(&outgoing->ip_hdr); |
| outgoing->udp_hdr.src_port = htons(ETH_AUDIO_SRC_PORT); |
| outgoing->udp_hdr.dst_port = htons(ETH_AUDIO_DST_PORT); |
| outgoing->udp_hdr.length = htons(ETH_AUDIO_PAYLOAD_SZ + UDP_HDR_SZ); |
| outgoing->udp_hdr.checksum = htons(0); |
| |
| /* Debugging */ |
| static int once = 0; |
| if (!once++) |
| printd("I will send %d bytes from %p:%d to %p:%d, iplen %04p, " |
| "udplen %04p\n", |
| ntohs(outgoing->udp_hdr.length), |
| ntohl(outgoing->ip_hdr.src_addr), |
| ntohs(outgoing->udp_hdr.src_port), |
| ntohl(outgoing->ip_hdr.dst_addr), |
| ntohs(outgoing->udp_hdr.dst_port), |
| ntohs(outgoing->ip_hdr.packet_len), |
| ntohs(outgoing->udp_hdr.length) |
| ); |
| } |
| |
| /* This is called by net subsys when it detects an ethernet audio packet. Make |
| * sure the in device has the contents, and send out the old frame. */ |
| void eth_audio_newpacket(void *buf) |
| { |
| struct page *in_page, *out_page; |
| /* Bail out if these two haven't been initialized yet */ |
| if (!ethaud_in || !ethaud_out) |
| return; |
| /* Put info from the packet into the outgoing packet */ |
| eth_audio_prep_response((struct ethaud_udp_packet*)buf, ð_udp_out); |
| in_page = (struct page*)ethaud_in->i_fs_info; |
| out_page = (struct page*)ethaud_out->i_fs_info; |
| /* third copy (1st being the NIC to RAM). */ |
| memcpy(page2kva(in_page), buf + ETH_AUDIO_HEADER_OFF, ETH_AUDIO_PAYLOAD_SZ); |
| /* Send the current outbound packet (can consider doing this by fsync) */ |
| eth_audio_sendpacket(page2kva(out_page)); |
| if (active_proc) |
| proc_notify(active_proc, 0); |
| } |
| |
| /* mmap() calls this to do any FS specific mmap() work. Since our files are |
| * defined to be only one page, we need to make sure they aren't mmapping too |
| * much, and that it isn't a PRIVATE mapping. |
| * |
| * Then we need to make sure the one page of the VMR is mapped in the page table |
| * (circumventing handle_page_fault and the page cache). Avoiding the page |
| * cache means we won't need to worry about accidentally unmapping this under |
| * pressure and having to remap it (which will cause a readpage). */ |
| int eth_audio_mmap(struct file *file, struct vm_region *vmr) |
| { |
| struct page *page = (struct page*)file->f_dentry->d_inode->i_fs_info; |
| /* Only allow mmaping from the start of the file */ |
| if (vmr->vm_foff) |
| return -1; |
| /* Only allow mmaping of a page */ |
| if (vmr->vm_end - vmr->vm_base != PGSIZE) |
| return -1; |
| /* No private mappings (would be ignored anyway) */ |
| if (vmr->vm_flags & MAP_PRIVATE) |
| return -1; |
| /* Only one proc can use it at a time (we need to know who to notify, since |
| * we don't have any sockets or other abstractions to work with). Note we |
| * are storing a reference. */ |
| if (active_proc && active_proc != vmr->vm_proc) |
| return -1; |
| if (!active_proc) { |
| kref_get(&vmr->vm_proc->p_kref, 1); |
| active_proc = vmr->vm_proc; |
| } |
| assert(page); |
| /* Get the PTE for the page this VMR represents */ |
| pte_t *pte = pgdir_walk(vmr->vm_proc->env_pgdir, (void*)vmr->vm_base, 1); |
| if (!pte) |
| return -ENOMEM; |
| /* If there was a page there, it should have be munmapp()d. */ |
| assert(!PAGE_PRESENT(*pte)); |
| /* update the page table */ |
| int pte_prot = (vmr->vm_prot & PROT_WRITE) ? PTE_USER_RW : |
| (vmr->vm_prot & (PROT_READ|PROT_EXEC)) ? PTE_USER_RO : 0; |
| /* Storing a reference to the page in the PTE */ |
| page_incref(page); |
| *pte = PTE(page2ppn(page), PTE_P | pte_prot); |
| return 0; |
| } |
| |
| /* This shouldn't be called. It could be if we ever handled a PF on the device, |
| * and the device wasn't present in the page table. This should not happen, |
| * since ea_mmap() should have sorted that out. Even if there was an unmap() |
| * then a new mmap(), it still shouldn't happen. */ |
| int eth_audio_readpage(struct page_map *pm, struct page *page) |
| { |
| warn("Eth audio readpage! (Did you page fault?)"); |
| return -1; |
| } |
| |
| /* Called when the file is about to be closed (file obj freed). This is the |
| * least intrusive way to find out when the device is unmapped and closed, so we |
| * can let go of the proc. TODO: Note that currently, the kernel will not be |
| * able to unmap the file by itself, due to the circular dependence of the kref, |
| * proc_free, and destroy_vmr. Programs have to unmap it themselves. Since |
| * this device is a bit of a hack anyway, I'm okay with not making big changes |
| * to the kernel to support this yet. */ |
| int eth_audio_release(struct inode *inode, struct file *file) |
| { |
| /* Disconnect the proc from the device, decref. */ |
| if (active_proc && current) { |
| assert(active_proc == current); |
| kref_put(&active_proc->p_kref); |
| active_proc = 0; |
| } |
| return 0; |
| } |
| |
| /* File operations */ |
| struct file_operations ethaud_in_f_op = { |
| dev_c_llseek, /* Errors out, can't llseek */ |
| 0, /* Can't read, only mmap */ |
| 0, /* Can't write, only mmap */ |
| kfs_readdir, /* this will fail gracefully */ |
| eth_audio_mmap, |
| kfs_open, |
| kfs_flush, |
| eth_audio_release, |
| 0, /* fsync - makes no sense */ |
| kfs_poll, |
| 0, /* readv */ |
| 0, /* writev */ |
| kfs_sendpage, |
| kfs_check_flags, |
| }; |
| |
| struct file_operations ethaud_out_f_op = { |
| dev_c_llseek, /* Errors out, can't llseek */ |
| 0, /* Can't read, only mmap */ |
| 0, /* Can't write, only mmap */ |
| kfs_readdir, /* this will fail gracefully */ |
| eth_audio_mmap, |
| kfs_open, |
| kfs_flush, |
| eth_audio_release, |
| 0, /* fsync - TODO: make this send the packet */ |
| kfs_poll, |
| 0, /* readv */ |
| 0, /* writev */ |
| kfs_sendpage, |
| kfs_check_flags, |
| }; |
| |
| /* Eth audio page map ops: */ |
| struct page_map_operations ethaud_pm_op = { |
| eth_audio_readpage, |
| }; |
| |
| #endif /* CONFIG_ETH_AUDIO */ |