
{"id":484,"date":"2020-01-18T15:11:09","date_gmt":"2020-01-18T15:11:09","guid":{"rendered":"http:\/\/timallen.name\/?p=484"},"modified":"2020-01-18T22:49:40","modified_gmt":"2020-01-18T22:49:40","slug":"linux-kernel-module-driver","status":"publish","type":"post","link":"https:\/\/timallen.name\/index.php\/2020\/01\/18\/linux-kernel-module-driver\/","title":{"rendered":"Linux Kernel Module (Driver)"},"content":{"rendered":"<p>&nbsp;<\/p>\n<pre class=\"lang:c decode:true  \" title=\"thing.h\">#ifndef thing_H\r\n#define thing_H\r\n\r\n#define DEVICE_NAME\t\t\t\t\"thing\"\r\n#define DRIVER_NAME\t\t\t\t\"thing\"\r\n\r\nstruct context {\r\n\tuint card;\t\t\t\t\t\t\/\/ cards probed\r\n\tstruct pci_dev *pdev;\t\t\t\/\/ pci dev\r\n\tvoid *buf1; \t\t\t\t\t\/\/ DMA download data\r\n\tdma_addr_t buf1_dma_handle;\t\t\/\/ physical address of the DMA download region\r\n\tstruct cdev cdev; \t\t\t\t\/\/ char dev\r\n\tuint opens; \t\t\t\t\t\/\/ XXX do I really need to know this?\r\n\tuint32_t pci_mem_start; \t\t\/\/ pci config space\r\n\tuint32_t pci_mem_length; \t\t\/\/ pci config space\r\n    void __iomem *bar0_base_addr; \t\/\/ the BARs were mapped here\r\n\tint irq; \t\t\t\t\t\t\/\/ our IRQ\r\n};\r\n\r\nstatic int thing_probe(struct pci_dev *, const struct pci_device_id *);\r\nstatic void thing_remove(struct pci_dev *);\r\nstatic int fops_open (struct inode *, struct file *);\r\nstatic ssize_t fops_read(struct file *, char __user *, size_t, loff_t *);\r\nstatic ssize_t fops_write(struct file *, const char __user *, size_t, loff_t *);\r\nstatic irqreturn_t thing_isr(int, void *);\r\nstatic int fops_release (struct inode *, struct file *);\r\n\r\n#endif \/\/thing_H<\/pre>\n<pre class=\"lang:c decode:true \" title=\"Linux Kernel Module (driver)\">#include &lt;linux\/cma.h&gt;\r\n#include &lt;linux\/device.h&gt;\r\n#include &lt;linux\/dma-mapping.h&gt;\r\n#include &lt;linux\/fs.h&gt;\r\n#include &lt;linux\/gfp.h&gt;\r\n#include &lt;linux\/init.h&gt;\r\n#include &lt;linux\/kernel.h&gt; \/* Needed for KERN_INFO *\/\r\n#include &lt;linux\/mm.h&gt;\r\n#include &lt;linux\/module.h&gt; \/* Needed by all modules *\/\r\n#include &lt;linux\/pci.h&gt;\r\n#include &lt;stddef.h&gt;\r\n#include &lt;linux\/cdev.h&gt;\r\n#include &lt;linux\/uaccess.h&gt;\r\n#include &lt;linux\/delay.h&gt;\r\n#include \"thing.h\"\r\n\r\n#define DEVICE_NAME \"thing\"\r\n#define DRIVER_NAME \"thing\"\r\n\r\nMODULE_LICENSE(\"GPL\");      \/\/ The license type -- this affects available functionality\r\nMODULE_AUTHOR(\"Tim Allen\"); \/\/ The author -- visible when you use modinfo\r\nMODULE_DESCRIPTION(\"thing accelerator Driver\");\r\nMODULE_VERSION(\"0.1\"); \/\/ A version number to inform users\r\n\r\nstatic const size_t dbsize = 16 * 1024;\r\nstatic struct class *class;\r\nstatic struct device *device_file;\r\nstatic struct context *ctxs[8]; \/\/up to 8 cards\r\n\r\n\/\/ define which file operations are supported\r\nstruct file_operations fops = {\r\n    .owner  = THIS_MODULE,\r\n    .llseek = NULL,\r\n    .read   = fops_read,\r\n    .write  = fops_write,\r\n    .poll   = NULL,\r\n    .unlocked_ioctl = NULL,\r\n    .mmap   = NULL,\t\t\t\t\t\/\/mmap\r\n    .open   = fops_open,\t\t\t\/\/open\r\n    .flush  = NULL,\r\n    .release = fops_release,\t\t\/\/close\r\n    .fsync  = NULL,\r\n    .fasync = NULL,\r\n    .lock   = NULL,\r\n};\r\n\r\n\r\nstatic const struct pci_device_id pci_device_ids[] = {\r\n  {PCI_DEVICE(0x1002, 0x6610)}, \/\/ some GPU card I'm using for dev\r\n  {PCI_DEVICE(0x1234, 0x000a)}, \/\/ hsp card\r\n  {PCI_DEVICE(0x1234, 0x000b)}, \/\/ our card\r\n  {0, \/* end of list *\/},\r\n};\r\n\r\nstatic struct pci_driver thing_driver = {\r\n  .name = DRIVER_NAME,\r\n  .id_table = pci_device_ids,\r\n  .probe = thing_probe,\r\n  .remove = thing_remove,\r\n};\r\n\r\n\/** init is the entry point for the module\r\n *\r\n *\/\r\nstatic int __init thing_init(void) {\r\n\tclass = class_create(THIS_MODULE, \"thing\");\r\n\tif(!class) {\r\n\t\tprintk (KERN_ERR \"[%s:init] unable to create class device\", DRIVER_NAME);\r\n\t\treturn -EIO;\r\n\t}\r\n\r\n\tif (pci_register_driver(&amp;thing_driver) &lt; 0) {\r\n\t\tprintk(KERN_ERR \"[%s:init] unable to register\", DRIVER_NAME);\r\n\t\treturn -EIO;\r\n\t}\r\n\tprintk(KERN_INFO \"[%s:init] driver registered\", DRIVER_NAME);\r\n\r\n\treturn 0; \/\/ A non 0 return means init_module failed; module can't be loaded.\r\n}\r\nmodule_init(thing_init); \/\/ this macro registers the module entry point\r\n\r\n\/** a card has been inserted, time to set it up\r\n *\r\n *\/\r\nstatic int thing_probe(struct pci_dev *pdev, const struct pci_device_id *id) {\r\n\tint err;\r\n\tuint8_t byte = 0;\r\n\tuint16_t word = 0;\r\n\tuint32_t dword = 0;\r\n\tchar devfilename[255];\r\n\tstruct context *ctx;\r\n\tstatic int cards = 0;\r\n\r\n\t\/\/begin filling out the context struct\r\n\tctx = kmalloc(sizeof (struct context), GFP_KERNEL);\r\n\tmemset(ctx, 0, sizeof (struct context));\r\n\tctxs[cards] = ctx; \/\/ store *ctx indexed by card probe order\r\n\tpci_set_drvdata(pdev, (void*)ctx); \/\/ store *ctx indexed by pdev\r\n\tctx-&gt;card = cards++; \/\/ card number in arbitrary order\r\n\tctx-&gt;pdev = pdev;\r\n\r\n\terr = pci_enable_device(pdev);\r\n\tif (err) {\r\n\t\tprintk(KERN_ERR \"[%s:probe] pci_enable_device returned %d\", DRIVER_NAME, err);\r\n        return -ENODEV;\r\n\t}\r\n\r\n\terr = pci_request_region(pdev, 0, DRIVER_NAME);\r\n\tif (err) {\r\n\t\tprintk(KERN_ERR \"[%s:probe] request_region returned %d\", DRIVER_NAME, err);\r\n\t\treturn -ENODEV;\r\n\t}\r\n\r\n\t\/\/ say some useless stuff about the card\r\n\tpci_read_config_word(pdev, PCI_VENDOR_ID, &amp;word);\r\n\tprintk(KERN_INFO \"[%s:probe] PCI_VENDOR_ID: %4.4x\", DRIVER_NAME, word);\r\n\tpci_read_config_word(pdev, PCI_DEVICE_ID, &amp;word);\r\n\tprintk(KERN_INFO \"[%s:probe] PCI_DEVICE_ID: %4.4x\", DRIVER_NAME, word);\r\n\tpci_read_config_word(pdev, PCI_COMMAND, &amp;word);\r\n\tprintk(KERN_INFO \"[%s:probe] PCI_COMMAND: %4.4x\", DRIVER_NAME, word);\r\n\tpci_read_config_word(pdev, PCI_STATUS, &amp;word);\r\n\tprintk(KERN_INFO \"[%s:probe] PCI_STATUS: %4.4x\", DRIVER_NAME, word);\r\n\tpci_read_config_byte(pdev, PCI_REVISION_ID, &amp;byte);\r\n\tprintk(KERN_INFO \"[%s:probe] PCI_REVISION_ID: %2.2x\", DRIVER_NAME, byte);\r\n\tpci_read_config_byte(pdev, PCI_INTERRUPT_LINE, &amp;byte);\r\n\tprintk(KERN_INFO \"[%s:probe] PCI_INTERRUPT_LINE: %2.2x\", DRIVER_NAME, byte);\r\n\tpci_read_config_byte(pdev, PCI_INTERRUPT_PIN, &amp;byte);\r\n\tprintk(KERN_INFO \"[%s:probe] PCI_INTERRUPT_PIN: %2.2x\", DRIVER_NAME, byte);\r\n\r\n\tctx-&gt;pci_mem_start = pci_resource_start(pdev, 0);\r\n    ctx-&gt;pci_mem_length = pci_resource_len(pdev, 0);\r\n\tpci_read_config_dword(pdev, ctx-&gt;pci_mem_start, &amp;dword);\r\n\tprintk(KERN_INFO \"[%s:probe] device_info: %8.8x\", DRIVER_NAME, dword);\r\n\tpci_read_config_dword(pdev, ctx-&gt;pci_mem_start + 0x3c, &amp;dword);\r\n\tprintk(KERN_INFO \"[%s:probe] revision_number: %8.8x\", DRIVER_NAME, dword);\r\n\r\n    ctx-&gt;bar0_base_addr = pci_iomap(pdev, 0, ctx-&gt;pci_mem_length); \/\/ map the BARs\r\n\tprintk(KERN_INFO \"[%s:probe] bar0_base_addr: %p\", DRIVER_NAME, ctx-&gt;bar0_base_addr);\r\n\r\n\t\/\/ create the device file entry (\/dev\/thing)\r\n\tsnprintf(devfilename, sizeof devfilename, \"thing%d\", ctx-&gt;card);\r\n\tdevice_file = device_create(class, NULL, MKDEV(810, ctx-&gt;card), 0, devfilename);\r\n\tif(!device_file) {\r\n\t\tprintk (KERN_ERR \"[%s:init] unable to create device class device\", DRIVER_NAME);\r\n\t\treturn -EIO;\r\n\t} else {\r\n\t\tprintk(KERN_INFO \"[%s:probe] device %s created.\", DRIVER_NAME, devfilename);\r\n\t}\r\n\r\n\t\/\/ set-up file ops\r\n\tcdev_init(&amp;ctx-&gt;cdev, &amp;fops);\r\n    ctx-&gt;cdev.owner = THIS_MODULE;\r\n    ctx-&gt;cdev.ops = &amp;fops;\r\n    err = cdev_add(&amp;ctx-&gt;cdev, MKDEV(810, ctx-&gt;card), 1);\r\n\tif (err) {\r\n\t\tprintk(KERN_INFO \"[%s:probe] cdev_add failed with %d\", DRIVER_NAME, err);\r\n\t\treturn -EIO;\r\n\t}\r\n\r\n\t\/\/ set-up the DMA buffers\r\n\terr = dma_set_mask_and_coherent(&amp;pdev-&gt;dev, DMA_BIT_MASK(32));\r\n\tif (err) {\r\n\t\tprintk(KERN_INFO \"[%s:probe] dma_set_mask returned: %d\", DRIVER_NAME, err);\r\n\t\treturn -EIO;\r\n\t}\r\n\tctx-&gt;buf1 = dma_alloc_coherent(&amp;pdev-&gt;dev, dbsize, &amp;ctx-&gt;buf1_dma_handle, GFP_KERNEL);\r\n\tif (!ctx-&gt;buf1) {\r\n\t\tprintk(KERN_ALERT \"[%s:probe] failed to allocate coherent buffer\", DRIVER_NAME);\r\n\t\treturn -EIO;\r\n\t}\r\n\tprintk(KERN_INFO \"[%s:probe] buf1 = %p buf1_dma_handle = %16.16llx \", DRIVER_NAME, ctx-&gt;buf1, ctx-&gt;buf1_dma_handle);\r\n\tiowrite32(ctx-&gt;buf1_dma_handle, ctx-&gt;bar0_base_addr + 0x140); \/\/ tell card where to DMA from\r\n\r\n\t\/\/ set-up IRQs\r\n\terr = pci_alloc_irq_vectors(pdev, 1, 1, PCI_IRQ_MSI);\r\n\tprintk(KERN_INFO \"[%s:probe] pci_alloc_irq_vectors returns %d\", DRIVER_NAME, err);\r\n\r\n\tctx-&gt;irq = pci_irq_vector(pdev, 0);\r\n\tprintk(KERN_INFO \"[%s:probe] pci_irq_vector returns %d\", DRIVER_NAME, ctx-&gt;irq);\r\n\r\n\terr = request_irq(ctx-&gt;irq, &amp;thing_isr, IRQF_SHARED, DRIVER_NAME, ctx);\r\n\tprintk(KERN_INFO \"[%s:probe] request_irq returns %d\", DRIVER_NAME, err);\r\n\r\n\t\/\/ config done\r\n\tdword = ioread32(ctx-&gt;bar0_base_addr + 0x00);\r\n\tdword |= 0x10000000; \/\/ config_done\r\n\tiowrite32(dword, ctx-&gt;bar0_base_addr + 0x00);\r\n\r\n\treturn 0; \/\/ A non 0 return means init_module failed; module can't be loaded.\r\n}\r\n\r\n\/** open function - called when the device file is opened\r\n *\r\n *\/\r\nstatic int fops_open (struct inode *inode, struct file *filp) {\r\n\tint major, minor;\r\n\tint card;\r\n\tstruct context *ctx;\r\n\r\n\tmajor = imajor(inode);\r\n\tcard = minor = iminor(inode);\r\n\tctx = ctxs[card];\r\n\r\n\tprintk(KERN_INFO \"[%s:open] major = %d\", DRIVER_NAME, major);\r\n\tprintk(KERN_INFO \"[%s:open] minor = %d\", DRIVER_NAME, minor);\r\n\r\n\tctx-&gt;opens++;\r\n\r\n\treturn 0;\r\n}\r\n\r\n\/** read function - called when the device file is written to\r\n *\r\n *\/\r\nstatic ssize_t fops_read(struct file *filp, char __user *ubuf, size_t count, loff_t *offp) {\r\n\tchar kbuf[512];\r\n\tuint c = 0; \/\/position in kbuf, start at 0\r\n\tint card; \/\/ the minor tells us which card\r\n\tuint8_t byte = 0;\r\n\tuint16_t word = 0;\r\n\tuint32_t dword =0;\r\n\tstruct context *ctx;\r\n\r\n\tcard = iminor(filp-&gt;f_inode);\r\n\tctx = ctxs[card];\r\n\r\n\tprintk(KERN_INFO \"[%s:read] minor = %d count = %ld *offp = %lld\", DRIVER_NAME, card, count, *offp);\r\n\tif (*offp)\r\n\t\treturn 0;\r\n\r\n\tc += snprintf(&amp;kbuf[c], sizeof kbuf, \"read from card\\t%d\\n\", card);\r\n\tpci_read_config_word(ctx-&gt;pdev, PCI_VENDOR_ID, &amp;word);\r\n\tc += snprintf(&amp;kbuf[c], sizeof kbuf, \"PCI_VENDOR_ID:\\t%4.4x\\n\", word);\r\n\tpci_read_config_word(ctx-&gt;pdev, PCI_DEVICE_ID, &amp;word);\r\n\tc += snprintf(&amp;kbuf[c], sizeof kbuf, \"PCI_DEVICE_ID:\\t%4.4x\\n\", word);\r\n\tpci_read_config_word(ctx-&gt;pdev, PCI_COMMAND, &amp;word);\r\n\tc += snprintf(&amp;kbuf[c], sizeof kbuf, \"PCI_COMMAND:\\t%4.4x\\n\", word);\r\n\tpci_read_config_word(ctx-&gt;pdev, PCI_STATUS, &amp;word);\r\n\tc += snprintf(&amp;kbuf[c], sizeof kbuf, \"PCI_STATUS:\\t%4.4x\\n\", word);\r\n\tpci_read_config_byte(ctx-&gt;pdev, PCI_REVISION_ID, &amp;byte);\r\n\tc += snprintf(&amp;kbuf[c], sizeof kbuf, \"PCI_REV_ID:\\t%2.2x\\n\", byte);\r\n\r\n\tdword = ioread32(ctx-&gt;bar0_base_addr + 0x00);\r\n\tc += snprintf(&amp;kbuf[c], sizeof kbuf, \"DEV NFO:\\t%8.8x\\n\", dword);\r\n\tdword = ioread32(ctx-&gt;bar0_base_addr + 0x04);\r\n\tc += snprintf(&amp;kbuf[c], sizeof kbuf, \"IRQ PND:\\t%8.8x\\n\", dword);\r\n\tdword = ioread32(ctx-&gt;bar0_base_addr + 0x08);\r\n\tc += snprintf(&amp;kbuf[c], sizeof kbuf, \"IRQ ACK:\\t%8.8x\\n\", dword);\r\n\tdword = ioread32(ctx-&gt;bar0_base_addr + 0x200);\r\n\tc += snprintf(&amp;kbuf[c], sizeof kbuf, \"FPGA Status:\\t%8.8x\\n\", dword);\r\n\tcopy_to_user(ubuf, kbuf, c);\r\n\t*offp += c;\r\n\treturn c;\r\n}\r\n\r\n\/** write function - called when the device file is written to\r\n *\r\n *\/\r\nstatic ssize_t fops_write(struct file *filp, const char __user *buffer, size_t length, loff_t * offset) {\r\n\tint card; \/\/ the minor tells us which card\r\n\tstruct context *ctx;\r\n\r\n\tcard = iminor(filp-&gt;f_inode);\r\n\tctx = ctxs[card];\r\n\r\n\tif (length%32)\r\n\t\tprintk(KERN_INFO \"[%s:write] WARNING: not a mutiple of 32, sending anyway\", DRIVER_NAME);\r\n\r\n\tif (length &gt; 16*32) {\r\n\t\tprintk(KERN_INFO \"[%s:write] ERROR: maximum of 16\", DRIVER_NAME);\r\n\t\treturn -EIO;\r\n\t}\r\n\r\n\tprintk(KERN_INFO \"[%s:write] sent %ld of 'em\", DRIVER_NAME, length \/ 32);\r\n\r\n\tmemset(ctx-&gt;buf1, 0, dbsize);\r\n\tcopy_from_user(ctx-&gt;buf1, buffer, length);\r\n\tiowrite32(length \/ 32, ctx-&gt;bar0_base_addr + 0x24); \/\/number of 32B (256b) words to be downloaded to the card\r\n\tiowrite32(0x80000000, ctx-&gt;bar0_base_addr + 0x20); \/\/Data0 Buffer and trigger\r\n\tndelay(250);\r\n\tiowrite32(0x00000000, ctx-&gt;bar0_base_addr + 0x20); \/\/Data0 Buffer and trigger\r\n\treturn length;\r\n}\r\n\r\n\/** Interrupt Service Routine\r\n *\r\n *\/\r\nstatic irqreturn_t thing_isr(int irq, void *lp) {\r\n\tint irq_pending;\r\n\r\n\t\/\/XXX ctxs, uh no, need em sorted by irq\r\n\tirq_pending = ioread32(ctxs[0]-&gt;bar0_base_addr + 0x04);\r\n\tprintk(KERN_INFO \"[%s:isr] RXd IRQ %d, irq_pending = %2.2x\\n\", DRIVER_NAME, irq, irq_pending);\r\n\tiowrite32(irq_pending, ctxs[0]-&gt;bar0_base_addr + 0x08);\r\n\r\n\treturn IRQ_HANDLED;\r\n}\r\n\r\n\/** close function - called when the device file is closed\r\n *\r\n *\/\r\nstatic int fops_release (struct inode *inode, struct file *filp) {\r\n\tint major, minor;\r\n\tint card;\r\n\tstruct context *ctx;\r\n\r\n\tmajor = imajor(inode);\r\n\tcard = minor = iminor(inode);\r\n\tctx = ctxs[card];\r\n\r\n\tprintk(KERN_INFO \"[%s:release] major = %d\", DRIVER_NAME, major);\r\n\tprintk(KERN_INFO \"[%s:release] minor = %d\", DRIVER_NAME, minor);\r\n\r\n\tctx-&gt;opens--;\r\n\treturn 0;\r\n}\r\n\r\n\/** remove a card\r\n *\r\n *\/\r\nstatic void thing_remove(struct pci_dev *pdev) {\r\n\tstruct context *ctx;\r\n\r\n\tctx = (struct context*) pci_get_drvdata(pdev);\r\n\tprintk(KERN_INFO \"[%s:remove] free coherent buffer\", DRIVER_NAME);\r\n\tif (ctx-&gt;buf1)\r\n\t\tdma_free_coherent(&amp;pdev-&gt;dev, dbsize, ctx-&gt;buf1, ctx-&gt;buf1_dma_handle);\r\n\r\n\tfree_irq(ctx-&gt;irq, ctx);\r\n\tpci_free_irq_vectors(pdev);\r\n\r\n\tdevice_destroy(class, MKDEV(810, ctx-&gt;card));\r\n\tcdev_del(&amp;ctx-&gt;cdev);\r\n\tpci_release_region(pdev, 0);\r\n\tpci_disable_device(pdev);\r\n\tkfree(ctx);\r\n}\r\n\r\n\/** prepare to be gone\r\n *\r\n *\/\r\nstatic void __exit thing_exit(void) {\r\n\tpci_unregister_driver(&amp;thing_driver);\r\n\tprintk(KERN_INFO \"[%s:exit] driver unregistered\", DRIVER_NAME);\r\n\tclass_unregister(class);\r\n\tprintk(KERN_INFO \"[%s:exit] Goodbye world\", DRIVER_NAME);\r\n}\r\nmodule_exit(thing_exit); \/\/ this macro registers the module exit point<\/pre>\n","protected":false},"excerpt":{"rendered":"<p>&nbsp; #ifndef thing_H #define thing_H #define DEVICE_NAME &#8220;thing&#8221; #define DRIVER_NAME &#8220;thing&#8221; struct context { uint card; \/\/ cards probed struct pci_dev *pdev; \/\/ pci dev void *buf1; \/\/ DMA download data dma_addr_t buf1_dma_handle; \/\/ physical address of the DMA download region struct cdev cdev; \/\/ char dev uint opens; \/\/ XXX do I really need &hellip; <a href=\"https:\/\/timallen.name\/index.php\/2020\/01\/18\/linux-kernel-module-driver\/\" class=\"more-link\">Continue reading <span class=\"screen-reader-text\">Linux Kernel Module (Driver)<\/span> <span class=\"meta-nav\">&rarr;<\/span><\/a><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[15],"tags":[],"class_list":["post-484","post","type-post","status-publish","format-standard","hentry","category-system-embedded"],"_links":{"self":[{"href":"https:\/\/timallen.name\/index.php\/wp-json\/wp\/v2\/posts\/484","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/timallen.name\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/timallen.name\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/timallen.name\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/timallen.name\/index.php\/wp-json\/wp\/v2\/comments?post=484"}],"version-history":[{"count":4,"href":"https:\/\/timallen.name\/index.php\/wp-json\/wp\/v2\/posts\/484\/revisions"}],"predecessor-version":[{"id":500,"href":"https:\/\/timallen.name\/index.php\/wp-json\/wp\/v2\/posts\/484\/revisions\/500"}],"wp:attachment":[{"href":"https:\/\/timallen.name\/index.php\/wp-json\/wp\/v2\/media?parent=484"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/timallen.name\/index.php\/wp-json\/wp\/v2\/categories?post=484"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/timallen.name\/index.php\/wp-json\/wp\/v2\/tags?post=484"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}