![]() |
Rick's b.log - 2015/03/23 |
It is the 1st of April 2025 You are 18.222.240.84, pleased to meet you! |
|
mailto:
blog -at- heyrick -dot- eu
The first is the application. This runs in User Mode, may be paged out (if a multitasking application), and is easy created using BASIC, C, Lua, etc.
The other type is the relocatable module. This (mostly) runs in Supervisor Mode (akin to kernel level), rarely interacts directly with the user, and is usually created in assembler or C.
The thing is, certain types of behaviour are not feasible for normal multitasking applications. Let's say we want to be like Windows and make a beep every time a USB device is connected. That's easy. We need to wait on the USB ServiceCall (&D2) for the attach message. This can be done in a regular application with the caveat that you unlink the service call handler prior to calling Wimp_Poll, and relink it afterwards.
Can you spot the flaw? That's right, you will only be able to see and respond to the Service Call whilst you are paged in.
You can't leave the thing set up across polls as the OS will attempt to call the Service Call handler at the address given. Which may be in a random part of some other application. Or it mightn't even be valid memory. At any rate, expect the machine to freeze if you do that.
There is an answer. A module. A piece of code that reacts to external events (via *Commands or SWIs or hooking into system events as described). It lives in the Relocatable Module Area (RMA) which is always mapped in to memory.
What we are going to do here is create a module. The module will be called USBeep and it will offer the following:
*USBeep_Status
which will tell you how many times it has beeped.
USBeep_Status
which will return how many times it has beeped in R0.
In this tutorial you should not copy-paste code. The stuff in bold typewriter text with light grey background
is the contents of the file. The stuff in normal text (like this) are comments and annotations pertaining to what is going on.
Don't worry, there are downloads below. It's the 21st century, nobody expects you to type out stuff any more!
Inside that directory, create three subdirectories: c, cmhg, and o.
This file is called CMHG.ModHeader.
; USBeep module definitions ; Module title/help title-string: USBeep help-string: USBeep 0.01 © 2015 Rick MurrayThere is nothing much to say about that. It is providing the expected module title and the string that will appear if you ask for help on the module. The help string also provides the version and some extra text that comes at the end of the line (the copyright symbol and stuff following).
; Module start and stop code initialisation-code: module_initialise finalisation-code: module_finaliseThis defines the function to use for module startup and shutdown. You must provide corresponding functions in your C code.
; SWI chunk and handler and list of SWIs [SWI chunk randomly chosen] swi-chunk-base-number: 0x45600 swi-handler-code: module_swihandler swi-decoding-table: USBeep, StatusThis tells CMHG that your SWI chunk will be &45600, the function that you must provide for dealing with this is module_swihandler, and the SWI name prefix is USBeep and one SWI (suffix Status) is defined.
; Service call handler for call &D2 so we can notice USB devices service-call-handler: module_servicecallhandler 0xD2Here we are telling CMHG that we are responding to Service Call &D2 (so it can generate the appropriate fast-discard case for filtering out unwanted Service Calls (plus the extension block), and it will be handled by a function called module_servicecallhandler.
; Veneer handler for Callback generic-veneers: module_veneer_callbackThis defines a generic veneer suitable for passing to OS_AddCallBack (etc).
Something that needs to be explained here is that you must not provide the function module_veneer_callback. This is provided by CMHG itself. So, wait, how do you deal will callbacks then?
Here is how it works. CMHG provides the function module_veneer_callback which you give to SWIs as the address to come to upon callback (event, etc). This small function will set up an environment suitable for the C runtime, and it will then automatically call the function module_veneer_callback_handler. This is the function that you provide. That, with "_handler" suffixed.
; *Commands. command-keyword-table: module_commands USBeep_Status(, min-args:0, max-args:0, invalid-syntax: "Syntax:\tUSBeep_Status\n", help-text: "Reports how many times this module has beeped.\n")The commands are handled by a function called module_commands. We define one, USBeep_Status, which takes no parameters, and has some help text.
; EndThat's all for this file.
Note: If you make modifications to the module source, don't forget to increment the version number here. This will cause the module header to be recreated which will have the pleasant side effect of embedding the correct build date. ☺
# Project: USBeep # Toolflags: CCflags = -c -depend !Depend -zM -I,C: -apcs 3/32bit -Wcd -throwback -fan Linkflags = -m -o $@ CMHGflags = -throwback -32bit ObjAsmflags = -APCS 3/32bit -desktop -Depend !Depend -ThrowBack -Stamp -IC:, Squeezeflags = -f -o $@ # C files used (referred by object file, linked in ORDER) c_files = o.module # Assembler files used s_files = # How to build the module @.USBeep: @.o.ModHeader $(c_files) $(s_files) C:o.stubs link $(linkflags) @.o.ModHeader $(c_files) $(s_files) C:o.stubs # How to build the module header @.o.ModHeader: @.cmhg.ModHeader cmhg $(cmhgflags) @.cmhg.ModHeader -o @.o.ModHeader # A macro for building the C code .c.o:; cc $(ccflags) $< -o $@ # A macro for building the assembler code .s.o:; objasm $(objasmflags) -from $< -to $@ # Dynamic dependencies:
/* USBeep module Version : 0.01 Date : Monday, 23rd March 2015 Copyright: © 2015 Richard MurrayNormally I would include a line saying what the purpose of the particular source is, such as "Handles USB transactions" or whatever. I do not write a list of changes into the tops of files. I include a file called Versions in the project directory and all changes are described there by release.*/
// Include everything we need #include "kernel.h" #include "swis.h" #includeInclude all the stuff that we need. This is all part of CLib so there's nothing unexpected here. Oh, and be aware that I use C++ style commenting extensively. That's okay, as the last compiler to not support that was Norcroft C v4 which doesn't work on modern systems.#include
// Some useful definitions #define UNUSED(x) (x = x) #define TRUE 1 #define FALSE 0The UNUSED macro is to stop the compiler throwing warnings. TRUE and FALSE are used to set the state of a flag.
// Register block for SWI calls _kernel_swi_regs r;Define a register block so we can call our own SWIs.
// Globals int connections = 0; // count of times a USB device has been connected int callback_pending = 0; // non-zero if a callback is pending. void *wsp = NULL; // Workspace pointer"Globals" is a bit silly in a one-source project, but you get the point...
// Error block definitions _kernel_oserror err_nousb = { 0x23400, "This machine doesn't appear to have USB!"}; _kernel_oserror err_noswi = { 0x23401, "Unknown USBeep SWI."};Again, I remind you, these numbers are completely made up and are not registered with ROOL. If you are making your own module, you should register your own stuff accordingly.
What would need to be registered for this module?
// Function prototype for CMHG-provided function extern void module_veneer_callback(void);This tells C about the veneer provided by CMHG.
Okay, here follows a safety trap that will remove a pending callback. Things can get unpleasant if the callback fires after the module has exit, so best not to let it happen.
// Safety trap void exit_tidyup(void) { if ( callback_pending ) { r.r[0] = (int)module_veneer_callback; r.r[1] = (int)wsp; _kernel_swi(OS_RemoveTickerEvent, &r, &r); callback_pending = 0; } return; }Notice how we provide the CMHG veneer module_veneer_callback to the SWI, as well as our private copy of the workspace pointer.
Now for module startup:
// Module Initialisation // ===================== int module_initialise(char *cmd_tail, int podule_base, void *pw) { UNUSED(cmd_tail); UNUSED(podule_base); // Remember work pointer for callback stuff wsp = pw;The parameters on entry are cmd_tail which points to any parameters following the module name (if any). This is so modules can be loaded and given options, though this behaviour is not usual. podule_base gives, I think, the base address of the podule that loaded this module. Probably not particularly relevant on newer systems that don't have podules! Finally pw is a pointer to our private word. We need to keep a copy of this.
The next job is to check that we have suitable USB support. Without USB functionality, our module is a bit, well, pointless.
// Check we have USB support // The SWI "USBDriver_Version" should resolve to *something*. r.r[1] = (int)"USBDriver_Version"; if ( _kernel_swi(OS_SWINumberFromString, &r, &r) != NULL ) return (int)&err_nousb; // return pointer to error block // Check we have at least version 0.60 of USBDriver _kernel_swi(0x54A45 /* USBDriver_Version */, &r, &r); if (r.r[0] < 60) return (int)&err_nousb; // return pointer to error block
Ensure that the tidyup routine is called at exit.
// Paranoia! atexit(exit_tidyup);For more accomplished modules, you may want to claim blocks of memory (in the RMA) here, and so on. However, our job is done.
// That's all for the startup return 0; // zero means all is good }
Here is the module shutdown handler. I think fatal flags the difference between *RMReInit
and *RMKill
. I tend to treat all shutdowns as being fatal... The podule value is no longer relevant. And pw is, as always, our private word.
What we do is call the tidyup function. I think this technically means it will be called twice (once here, once by atexit()), but this is not a problem, the second time will have no effect as any pending callback will have been cleared.
// Module finalisation // =================== // _kernel_oserror *module_finalise(int fatal, int podule, void *pw) { UNUSED(fatal); UNUSED(podule); UNUSED(pw); // Ensure and pending callback is suppressed exit_tidyup(); // That's all. return 0; }
Here follows the SWI handler. swi is the SWI number MOD 63 (that means it starts at 0). The register block ir is the registers given to the command. Note the '*' prefix, you must use indirection here - this means the register is ir->r[x]
and not the usual form (like ir.r[x]
). You can update the register block and anything you modify will be changed prior to the SWI returning. Finally, pw is, as always, the private word.
There isn't much to say here. If the SWI is zero, it is USBeep_Status so we set R0 (updated on SWI exit) to the number of connections. If the SWI is something else, we throw an error.
// SWI handler // =========== // _kernel_oserror *module_swihandler(int swi, _kernel_swi_regs *ir, void *pw) { // SWI handler. UNUSED(pw); // Thenumber counts from zero. switch (swi) { case 0: // USBeep_Status // IN: - // OUT: R0 = count of beeps made ir->r[0] = connections; break; default: // anything else - throw an error return (_kernel_oserror *)&err_noswi; break; // not necessary but karmically pleasing } return NULL; }
Like the SWI handler, the command handler takes a variable command that counts from zero. The array arg_string and value argc provide a list of parameters to the command (if applicable) which works in the same way as the traditional argv/argc pair on entry to the usual main function.
In case you were wondering, there is no main()
as this code is reactive, not interactive. It is possible to have a main()
in a module, but that's way out of the scope of this tutorial!
The only complication here is looking at the number of USB connections in order to provide a suitable response. One thing that really annoys me is when I see software providing prompts like "You have 1 messages" because it is such a simple thing to deal with. It's a bit long winded here as the grammar changes. A correct 's' suffix? I've done it in assembler in a mere two instructions (a CMP followed by a conditional SWI to output an S). Sorry. Small things like that irritate me. I'm not perfect... ;-)
// *Command handler // ================ // _kernel_oserror *module_commands(char *arg_string, int argc, int command, void *pw) { UNUSED(pw); UNUSED(argc); UNUSED(arg_string); // Commands count from zero. // *USBeep_Status if (command == 0) { if ( connections == 0 ) printf("No USB devices have been connected.\n"); else if ( connections == 1 ) printf("There has been 1 USB connection.\n"); else printf("There have been %d USB connections.\n", connections); } return NULL; }
Here is the Service Call handler. We may be called in IRQ mode. RISC OS is threaded. It is really really unsafe to do much of anything in this state - especially you cannot use SWIs that are not re-entrant which precludes the use of OS_WriteC and other VDU/printing calls plus anything to do with FileCore (check the PRMs for other OS SWIs); so what happens here is we simply check to see if this matches up with the conditions we are looking for (Service Call &D2, reason code 0) and if it does the connections value will be incremented and if there is no callback pending, one will be requested. That's about all we can do here.
// ServiceCall handler // =================== // void module_servicecallhandler(int service, _kernel_swi_regs *ir, void *pw) { // Service Call handler // // NOTE SPECIFIC CONDITIONS OF ENTRY - it is NOT safe to do much of anything, // so if something happens that we need to respond to, do the bare minimum that // is actually necessary and then throw a callback and do the rest there. // // RISC OS IS THREADED: ABSOLUTELY DO NOT PRINT TO THE SCREEN OR DO FILE OPS. // // We hang on to this Service Call: // // &D2 Service_USBDriver // R0 = Reason (see below) // R1 = &D2 // R2 ... // // Reason = R0 = 0 = New device attached to the USB system. // R2 is a pointer to USB device descriptor block. // --> This is what we are looking for. <-- // R0 = 1 = List connected devices (see usb_findmidi() ). // R2 is a pointer to linked list of descriptor blocks. // R0 = 2 = Unused // R0 = 3 = USB system is (re)starting. // R0 = 4 = USB system is dying. UNUSED(pw); if (service == 0xD2) { // USB driver if (ir->r[0] == 0) { // New device connected, time to do something! connections++; // we actually count connections, not beeps // The beep is a VDU operation, so we'll do this on a callback if ( !callback_pending ) { r.r[0] = (int)module_veneer_callback; r.r[1] = (int)wsp; _kernel_swi(OS_AddCallBack, &r, &r); callback_pending = TRUE; } } } return; }
And finally... the callback handler. RISC OS does not provide a guaranteed time for when the callback will happen. It will take place when RISC OS is no longer threaded - that means it would return to User Mode with the SVC stack empty or in some cases when the system is idling. We are called in SVC mode as we're a module. All we do is issue a beep the old-school (VDU7) way.
// Callback handler // ================ // _kernel_oserror *module_veneer_callback_handler(_kernel_swi_regs *ir, void *pw) { // RISC OS is not threaded, so we can do stuff here. UNUSED(pw); UNUSED(ir); // Just make a beep _kernel_oswrch(7); // VDU 7 callback_pending = FALSE; return NULL; }
For extra credit: My original code here did the following and it made a beep and then stiffed the machine. Why did this happen?
// Just make a beep __asm { SWI 256+7 }
I have demonstrated SWIs, commands, Service Calls, and using callbacks. I hope I have covered most of the important stuff in a module that does practically nothing. But, hey, it is supposed to be a tutorial to show how all this stuff fits together.
I hope this has been helpful, and if it has, please go to the comments below and let me know what you've done with this.
No comments yet...
© 2015 Rick Murray |
This web page is licenced for your personal, private, non-commercial use only. No automated processing by advertising systems is permitted. RIPA notice: No consent is given for interception of page transmission. |