Browse Source

introduce init.md, grammer fixes, center author, temp-control state machine

Pat Beirne 1 month ago
parent
commit
0ede4e93ad
4 changed files with 284 additions and 35 deletions
  1. 58 31
      CooperativeMultitasking.md
  2. 2 2
      makefile
  3. 2 2
      pandoc.css
  4. 222 0
      uart.md

+ 58 - 31
CooperativeMultitasking.md

@@ -5,7 +5,7 @@ email: <patb@pbeirne.com>
 date: 2025/01/15
 date: 2025/01/15
 license: MIT
 license: MIT
 ---
 ---
-<style> h2 { border-top: solid thin #899;}   </style>
+<style> h2 { border-top: solid thin #899;} summary {color: #08b;}  </style>
 # Part 1: Cooperative Multitasking
 # Part 1: Cooperative Multitasking
 
 
 <!-- ## Introduction -->
 <!-- ## Introduction -->
@@ -48,26 +48,28 @@ It helps to have a passing knowledge of how to connect a transducer
 <details><summary>Reality Check</summary>
 <details><summary>Reality Check</summary>
 The technique described here is also called ***event driven programming***.
 The technique described here is also called ***event driven programming***.
 
 
-This technique was used in the original Window (1995),
-including some of the system calls: `sendMessage(), postMessage() and setTimer()`.
+This technique was used in the original Windows (1995),
+and includes some of the system calls used in this paper: 
+`sendMessage(), postMessage() and setTimer()`.
 
 
-This event-driven technique is applicable to a whole host 
+This event-driven technique is applicable to a whole array 
 of small microcontrollers, including
 of small microcontrollers, including
 
 
 - MSP430
 - MSP430
-- Cortex M0, M0+ (SAM, STM32, PY32, Cypress, Kinetis, HT32, XMC, LPC81x)
-- AtMega, AtTiny
+- Cortex M0, M0+ (SAM, STM32, PY32, Cypress, Kinetis, HT32, XMC, LPC81x, CH32V)
+- AtMega, AtTiny, LGT8F
 - 8051 (SiliconLabs, Nuvoton, HT85)
 - 8051 (SiliconLabs, Nuvoton, HT85)
 - RL78 (Renesas)
 - RL78 (Renesas)
 - Pic 12/14/16
 - Pic 12/14/16
 - Risc (ch32v)
 - Risc (ch32v)
 - STM8
 - STM8
 
 
-*Some of the really tiny ones don't have enough stack space to implement these
-techniques (Puolop PB150, Padauk PxS15x, Bojuxing BJ8P, 
+*Some of the really tiny ones probably don't have enough stack space to implement the
+technique described in this paper (Puolop PB150, Padauk PxS15x, Bojuxing BJ8P, 
 Yspring MDT1x, EastSoft HR7P, Holtek Ht68.)*
 Yspring MDT1x, EastSoft HR7P, Holtek Ht68.)*
 
 
-![Here](blb.jpg) is a typical board hosting 12 buttons, 12 RGB LEDs, all hosted
+![button & lights board](blb.jpg) This is a typical board hosting 12 buttons, 
+12 RGB LEDs, all hosted
 by an STM32. Every light and button can be controlled separately 
 by an STM32. Every light and button can be controlled separately 
 and simultaneously, using the technique described in this paper.
 and simultaneously, using the technique described in this paper.
 
 
@@ -117,9 +119,9 @@ Of course, we will need to write the code for `setup_hardware()`
 and `setup_interrupts()`. 
 and `setup_interrupts()`. 
 And `flashLedTask()` and `respondToButtonTask()`. And create the *magic* 
 And `flashLedTask()` and `respondToButtonTask()`. And create the *magic* 
 that allows `event` information to flow.
 that allows `event` information to flow.
-Don't worry, it will all be laid out in the following pages. 
+All this will be laid out in the following pages. 
 
 
-If you're concerned with complexity, feel free to jump ahead to the 
+If you're concerned with complexity, feel free to jump ahead about 10 pages to the 
 [final implementation](#final-implementation) page to see real, tested, code.
 [final implementation](#final-implementation) page to see real, tested, code.
 
 
 Between here and there, I'll walk you step-by-step through building 
 Between here and there, I'll walk you step-by-step through building 
@@ -145,7 +147,7 @@ Each interrupt causes the execution of *interrupt handler* code. For this projec
 something like this:
 something like this:
 
 
 ```C
 ```C
-enum {EVT_NONE, EVT_TICK, EVT_BUTTON};
+enum {EVT_NONE=0, EVT_TICK, EVT_BUTTON};
 
 
 INTERRUPT timer_isr(void) {
 INTERRUPT timer_isr(void) {
   newEvent(EVT_TICK);
   newEvent(EVT_TICK);
@@ -171,6 +173,7 @@ cases, there's no need to ack. Specifically,
 the SYS_TICK interrupt in the ARM Cortex processors does *not*
 the SYS_TICK interrupt in the ARM Cortex processors does *not*
 need an ack, so the above code example for `timer_isr()` is complete.
 need an ack, so the above code example for `timer_isr()` is complete.
 
 
+<hr>
 When a pushbutton or switch is connected to a microcontroller, 
 When a pushbutton or switch is connected to a microcontroller, 
 the first bit of activity will
 the first bit of activity will
 cause the interrupt and execute the `button_isr()` code. 
 cause the interrupt and execute the `button_isr()` code. 
@@ -178,11 +181,12 @@ However, real buttons produce about 5msec of
 *bounce* and this will cause subsequent interrupts unless 
 *bounce* and this will cause subsequent interrupts unless 
 they are somehow filtered out. There are
 they are somehow filtered out. There are
 lots of ways to handle *bounce*, and I'll let you read about that 
 lots of ways to handle *bounce*, and I'll let you read about that 
-[elsewhere]](#debouncing). Most techniques boil
+[elsewhere](#debouncing). Most techniques boil
 down to either ignoring subsequent interrupts (from the same button) 
 down to either ignoring subsequent interrupts (from the same button) 
 for about 5 msec, or disabling
 for about 5 msec, or disabling
 that specific interrupt until the 5msec has passed.
 that specific interrupt until the 5msec has passed.
 
 
+<hr>
 As a general rule, *input* pins should be observed either by 
 As a general rule, *input* pins should be observed either by 
 interrupt service routine (ISR), or scanned
 interrupt service routine (ISR), or scanned
 periodically by the timer ISR. *Outputs* should be 
 periodically by the timer ISR. *Outputs* should be 
@@ -195,7 +199,7 @@ controlled in the task code, which we'll see below.
 An *event* in this context is a small bit of information that appears 
 An *event* in this context is a small bit of information that appears 
 asynchronously in the system.
 asynchronously in the system.
 Implemented, it can be a *byte*, or an *int* or an even larger structure. 
 Implemented, it can be a *byte*, or an *int* or an even larger structure. 
-But in these small microcontrollers, let's use a *byte*.
+But in these small microcontrollers, let's use a byte.
 
 
 ```C
 ```C
 volatile uchar event;
 volatile uchar event;
@@ -206,11 +210,10 @@ and processed at the main level. We use the `event` object to send
 information between these levels, so we have to mark it 
 information between these levels, so we have to mark it 
 [`volatile`](https://en.wikipedia.org/wiki/Volatile_(computer_programming)).
 [`volatile`](https://en.wikipedia.org/wiki/Volatile_(computer_programming)).
 
 
-In this paper, the word *message* and *event* are equivalent. 
-*Message* has the sense of 
+In this paper, the words *event* and *message* are equivalent. 
+*Event* has the sense of "something external just happened" and 
+*message* has the sense of 
 "a bit of communication from another part of the program" 
 "a bit of communication from another part of the program" 
-and *event* has the sense of "something
-external just happened". 
 At this point in the design, they both mean the same thing.
 At this point in the design, they both mean the same thing.
 
 
 By convention, let's use *zero* to indicate the absence of an event/message, 
 By convention, let's use *zero* to indicate the absence of an event/message, 
@@ -219,14 +222,14 @@ an event/message.
 
 
 ```C
 ```C
 volatile uchar event;
 volatile uchar event;
-enum {EVT_NONE, EVT_TICK, EVT_BUTTON}; // 0,1,2
+enum {EVT_NONE=0, EVT_TICK, EVT_BUTTON}; // 0,1,2
 ```
 ```
 
 
 <details>
 <details>
 <summary>Reality check</summary>
 <summary>Reality check</summary>
 
 
 <hr>
 <hr>
-For this project, let's suppose the timer ticks happen every 10ms (100 per second).
+For this example project, let's suppose the timer ticks happen every 10ms (100 per second).
 <hr>
 <hr>
 
 
 </details>
 </details>
@@ -283,12 +286,12 @@ Here's a more realistic version:
 
 
 ```C
 ```C
 void newEvent(char evt) {
 void newEvent(char evt) {
-  static uchar nextEvent;   // keep track of where we are in queue
+  static uchar nextEvent;           // keep track of where we are in queue
   disable_irq();                    // critical section
   disable_irq();                    // critical section
   events[nextEvent++] = evt;        // insert event into queue
   events[nextEvent++] = evt;        // insert event into queue
   if (nextEvent == NUM_EVENTS)      // loop back to the start of queue
   if (nextEvent == NUM_EVENTS)      // loop back to the start of queue
     nextEvent = 0;
     nextEvent = 0;
-  enable_irq();           // end critical section, probably <100us of blockage
+  enable_irq();                     // end critical section, probably <100us of blockage
 }
 }
 ```
 ```
 </details>
 </details>
@@ -312,11 +315,7 @@ To see the real code for the dispatcher, [jump ahead](#dispatcher-details).
 The `main()` code illustrated here calls the tasks 
 The `main()` code illustrated here calls the tasks 
 as subroutines, sending each one a copy of the event number. 
 as subroutines, sending each one a copy of the event number. 
 
 
-As I mentioned earlier, *events* can also be referred to as *messages*. 
-We can also refer to this process "sending
-a message to the task"
-
-So, for this paper, these are equivalent
+For this paper, these are equivalent
 
 
 - calling a task subroutine with the event information
 - calling a task subroutine with the event information
 - sending a message to a task
 - sending a message to a task
@@ -372,7 +371,7 @@ state, which can be represented in a net diagram.
 Here's a state diagram for the task which reacts to a button press by
 Here's a state diagram for the task which reacts to a button press by
 flashing an LED.
 flashing an LED.
 
 
-![simple state diagram](rtb_state.png)
+![simple state diagram: RTB=respond to button](rtb_state.png)
 
 
 How does the *state code* remember what state it's in between invocations?
 How does the *state code* remember what state it's in between invocations?
 We can use a `static` variable. A `static` is stored in main memory (*not 
 We can use a `static` variable. A `static` is stored in main memory (*not 
@@ -407,7 +406,8 @@ void respondToButtonTask(uchar evt) {
 }
 }
 ```
 ```
 
 
-Each time this routine is called, it checks the *event* that it was given, 
+Each time this routine is called, it first forks, depending on the current state.
+Then it checks the *event* that it was given, 
 and sometimes processes it. And sometimes it changes state.
 and sometimes processes it. And sometimes it changes state.
 
 
 Here's a few things to notice:
 Here's a few things to notice:
@@ -694,7 +694,7 @@ What's missing from this code sample is the initialization of the
 hardware........some kind of `setup()` routine. I left it out because
 hardware........some kind of `setup()` routine. I left it out because
 it's going to vary so much between processors, and only distracts from the
 it's going to vary so much between processors, and only distracts from the
 point of this paper. If you want a detailed, tested copy of this code,
 point of this paper. If you want a detailed, tested copy of this code,
-see [here](build/demo.c)
+see it [here](#real-code) or download it [here](build/demo.c)
 
 
 The above example only shows 2 tasks, so all the formal structure may
 The above example only shows 2 tasks, so all the formal structure may
 seem a bit silly. But once you have the infrastructure in place, you can
 seem a bit silly. But once you have the infrastructure in place, you can
@@ -889,7 +889,6 @@ handle buffering the events until they can be processed.
 
 
 Under no circumstances should a task take more than 100msec. Use a new state,
 Under no circumstances should a task take more than 100msec. Use a new state,
 and return from the task. Process the new state later.
 and return from the task. Process the new state later.
-<hr>
 </details>
 </details>
 
 
 
 
@@ -1870,3 +1869,31 @@ void checkRadioFreq() {
 This may seem like a lot of code, but it compiles to something quite small.
 This may seem like a lot of code, but it compiles to something quite small.
 It is non-blocking, and easily maintained and modified. And it works.
 It is non-blocking, and easily maintained and modified. And it works.
 
 
+### Temperature Control
+
+Another example would be a user interface that had two buttons, 
+marked "up" and "down", an LED marked "setting", 
+some sort of numeric display, and some output
+device which needs to be controlled, such as a heater element, a motor on 
+a mixer or even a volume setting on a sound system.
+
+The user presses the "up" or "down" button, and the "setting: LED lights, 
+to inform the user. Further presses of "up" or "down" modify the
+numeric display, but don't yet affect the device which is being controlled.
+
+When the users pauses for 1.5 seconds, the LED is extinguished and 
+the new setting is used to control the output device.
+
+The state diagram might look something like 
+![heater control state machine](up_down.png)
+
+
+## UART Manager
+
+It's pretty common for these small microprocessors to have a UART service
+builtin. And it's also pretty common for these small micros to be used as
+a slave to a more sophisticated master computer. 
+
+So I have written a [UART streaming service as a task](uart.html) 
+which fits into 
+this *Cooperative Multitasking* (*event driven*) operating system. 

+ 2 - 2
makefile

@@ -1,8 +1,8 @@
-SOURCES=CooperativeMultitasking.md
+SOURCES=CooperativeMultitasking.md uart.md
 IMG_SOURCES=led_state.dot led_state_simple.dot rtb_state.dot fridge_state.dot \
 IMG_SOURCES=led_state.dot led_state_simple.dot rtb_state.dot fridge_state.dot \
   rtb_state_2.dot rtb_state_3.dot rtb_state_4.dot rtb_state_5.dot \
   rtb_state_2.dot rtb_state_3.dot rtb_state_4.dot rtb_state_5.dot \
   window_state.dot radio_tuner.dot beer_vat.dot \
   window_state.dot radio_tuner.dot beer_vat.dot \
-  window_complex.dot door.dot door2.dot vending.dot
+  window_complex.dot door.dot door2.dot vending.dot up_down.dot
 
 
 
 
 OBJECTS=$(SOURCES:.md=.html)
 OBJECTS=$(SOURCES:.md=.html)

+ 2 - 2
pandoc.css

@@ -160,7 +160,7 @@ p .footnote:first-of-type {
 	}
 	}
 
 
 	/* Title */
 	/* Title */
-	.title, .subtitle, .authors, .date {
+	.title, .subtitle, .authors, .author, .date {
 		text-align: center;
 		text-align: center;
 		padding: 0em;
 		padding: 0em;
 		margin: 0em;
 		margin: 0em;
@@ -178,7 +178,7 @@ p .footnote:first-of-type {
 		display: none;
 		display: none;
 	}
 	}
 
 
-	.authors, .date {
+	.authors, .date, .author{
 		font-size: 12pt;
 		font-size: 12pt;
 		margin: 1em 0em;
 		margin: 1em 0em;
 	}
 	}

+ 222 - 0
uart.md

@@ -0,0 +1,222 @@
+---
+title: UART Manager
+author: Pat Beirne
+email: <patb@pbeirne.com>
+date: 2025/01/30
+licence: MIT
+---
+
+> This is not a state machine, per se. This page is included because
+> I found it very common to need UART communication, and I found I could
+> fit it into the *event-dispatcher* system pretty easily.
+
+Suppose we have a project which needs to use the UART to send and receive
+information. The outbound stream contains diagnostic information 
+and measured parameters; the *report stream* is composed of 
+chunks of up to 100 bytes at a time.
+Output reports can be generated by any task, 
+and the chunks of data must not be interleaved.
+
+The *input* stream consists of *command* chunks of 1-10 bytes, used for testing, for
+remote control and for injecting input stimuli.
+
+> When data arrives (a command), all tasks are informed with an `EVT_CHAR`.
+> The tasks can then query the UART service routines to see if they
+> need to process the input.
+
+<details><summary>Reality Check</summary>
+I recommend sending ASCII chars through the UART. 
+It makes debugging ***way*** easier.
+
+For some of my projects, I use fixed-length packets, where the first characters
+determine the packet length. For example:
+
+
+**Master -> UART -> Slave (this processor) **
+
+| command | syntax | meaning | 
+| --- | --- | --- |
+| light LED | Lnf | set LED {n} either {f==0} off or {f==1} on |
+| beep | Bn | issue a beep for {n} x100 ms |
+| inject key press | kn | simulate key {n} press |
+| inject key release | Kn | simulate key {n} release |
+
+
+**Slave (this processor) -> UART -> Master **
+
+| report | syntax | meaning |
+| --- | --- | --- |
+| keypress | kn | key {n} is pressed |
+| keyrelease | Kn | key {n} is released |
+| debug string | Dnxxxxxxxx | general debugging, of length {n} |
+| init finished | I | the startup process is finished, processor ready |
+ 
+> {n} is one of `"0123456789ABCDE...XYZabcd.....xyz"`
+
+</details>
+
+We create some buffers in RAM to hold the characters until they can
+be sent or processed:
+
+```C
+#define TX_SIZE 256
+#define RX_SIZE 32
+
+/* circular buffers */
+
+char tx_buffer[TX_SIZE];        // the largest packet we need to tx is 50 bytes
+char *tx_next_empty = tx_buffer, *tx_next_send = tx_buffer;
+const char * const tx_end = tx_buffer + TX_SIZE;
+
+
+char rx_buffer[RX_SIZE];        // the largest packet we need to rx is 192 bytes
+char *rx_next_empty = rx_buffer, *rx_next_rcv = rx_buffer;
+const char * const rx_end = rx_buffer + RX_SIZE;
+```
+
+This task will have a few service routines which any task can access
+at any time:
+
+```C
+const char uartGetChar(void);       // returns -1 for no-char-available
+const char uartCheckChar(void);     // leave char in buffer
+const int  uartGetCharCount(void);
+void       uartDiscardChars(int n);
+const char uartSendChar(char c);    // return -1 if buffer is full
+const int  uartSendString(const char* p, int len);
+```
+
+The UART itself operates at the interrupt level; we get distinct interrupts
+for *rx data ready* and *tx buffer empty*.
+
+The `uartSendChar()` pushes a char into the tx buffer. The first character
+inserted causes the UART hardware to send; subsequent chars are simply
+pushed into the `tx_buffer`. When the UART is finished 
+sending, it triggers an interrupt which checks the `tx_buffer` for more
+bytes and sends them, until the buffer is emptied. 
+
+Characters are inserted into the `tx_buffer` in order, and
+as long as the entire packet of data is inserted during the same
+task process (EVT_TICK or whatever), then those bytes will remain adjacent.
+
+The `rx_buffer` is filled automatically by the rx interrupt. The rx interrupt
+also generates the `EVT_CHAR` message to inform all the tasks that rx data
+is ready. 
+
+The `uartGetChar()` checks the rx buffer, and returns a char if one is available 
+(-1 otherwise). The `uartCheckChar()` and `uartGetCharCount()`
+let the designer check the first character of a command string, and determine 
+whether the command has been fully loaded into the rxBuffer.
+
+Here is a fully functional module that implements the above:
+
+```C
+
+#define TX_SIZE 256
+#define RX_SIZE 32
+
+/* circular buffers */
+
+char tx_buffer[TX_SIZE];        // the largest packet we need to tx is 50 bytes
+char *tx_next_empty = tx_buffer, *tx_next_send = tx_buffer;
+const char * const tx_end = tx_buffer + TX_SIZE;
+
+
+char rx_buffer[RX_SIZE];        // the largest packet we need to rx is 192 bytes
+char *rx_next_empty = rx_buffer, *rx_next_rcv = rx_buffer;
+const char * const rx_end = rx_buffer + RX_SIZE;
+```
+
+<details><summary>STM32F0xx specific init code</summary>
+
+```C
+void initUart(void) {
+
+        USART2->CR2 = 0;                // no auto baud on USART2 :(
+        USART2->CR3 = 0;
+//      USART2->BRR = 0x341;    // from 8MHz clock 16x oversample
+        USART2->BRR = 0x44;             // for 115200
+        USART2->CR1 = 0x016AD;  // 8 bits, even parity, tx/rx/usart enable + tx/rx interrupts
+        NVIC->ISER[0] |= (1<<28);       // and enable the NVIC
+}       
+```
+</details>
+
+```C
+const char uartGetChar(void) {
+    if (rx_next_rcv == rx_next_empty)
+        return -1;      // no char available
+    else {
+        char c = *rx_next_rcv++;
+        if (rx_next_rcv >= rx_end)
+            rx_next_rcv = rx_buffer; // wrap to beginning
+        return c;
+    }
+} 
+
+const char uartCheckChar(void) {
+    if (rx_next_rcv == rx_next_empty)
+        return -1;      // no char available
+    else 
+        return *rx_next_rcv;
+} 
+
+int uartGetCharCount(void) {
+    int ret = rx_next_empty - rx_next_rcv;
+    if (ret<0)
+        ret += RX_SIZE;
+    return ret;
+}
+
+void uartDiscardChar(int i) {
+    while (i--)
+        uartGetChar();
+}
+
+/* the for a RX character
+ * deposit it into the buffer, issue a EVT_CHAR and return
+ */ 
+void uart_rx_isr(void) {
+  // rx data is ready, read it.....status bit auto-clears
+  *rx_next_empty++ = USART2->RDR;
+  if (rx_next_empty >= rx_end)  // wrap back to beginning
+    rx_next_empty = rx_buffer;
+  newEvent(EVT_CHAR);     // inform all tasks that there is 
+                          // something in the rx buffer
+}
+void uart_tx_isr(void) {
+    // the uart tx is empty
+    // if there is tx data available to send, push it into the uart
+    if (tx_next_send != tx_next_empty) {
+      USART2->TDR = *tx_next_send;
+      if (++tx_next_send >= tx_end) // wrap back to beginning
+        tx_next_send = tx_buffer;
+    }
+    // else, just disable the interrupt
+    else
+      USART2->CR1 &= ~0x80;  
+}
+
+
+/** add character to the tx buffer
+ * @return -1 if the buffer is full
+ * */
+char uartSendChar(char c) {
+    *tx_next_empty = c;             // increment AFTER adding to queue
+    if (++tx_next_empty >= tx_end)
+        tx_next_empty = tx_buffer;
+    if (tx_next_empty == tx_next_send) 
+        c = -1;
+    // if the USART is empty, trigger the first send
+    if (USART2->ISR & 0x80) 
+        USART2->CR1 |= 0x80;            // enable the tx interrupt
+    return c;
+}
+
+/** send a string a specified length out the uart */
+void uartSendString(const char* p, int len) {
+        while (len--)
+                uartSendChar(*p++);
+}       
+```
+