Parcourir la source

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

Pat Beirne il y a 1 mois
Parent
commit
0ede4e93ad
4 fichiers modifiés avec 284 ajouts et 35 suppressions
  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
 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
 
 <!-- ## Introduction -->
@@ -48,26 +48,28 @@ It helps to have a passing knowledge of how to connect a transducer
 <details><summary>Reality Check</summary>
 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
 
 - 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)
 - RL78 (Renesas)
 - Pic 12/14/16
 - Risc (ch32v)
 - 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.)*
 
-![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 
 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 `flashLedTask()` and `respondToButtonTask()`. And create the *magic* 
 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.
 
 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:
 
 ```C
-enum {EVT_NONE, EVT_TICK, EVT_BUTTON};
+enum {EVT_NONE=0, EVT_TICK, EVT_BUTTON};
 
 INTERRUPT timer_isr(void) {
   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*
 need an ack, so the above code example for `timer_isr()` is complete.
 
+<hr>
 When a pushbutton or switch is connected to a microcontroller, 
 the first bit of activity will
 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 
 they are somehow filtered out. There are
 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) 
 for about 5 msec, or disabling
 that specific interrupt until the 5msec has passed.
 
+<hr>
 As a general rule, *input* pins should be observed either by 
 interrupt service routine (ISR), or scanned
 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 
 asynchronously in the system.
 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
 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 
 [`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" 
-and *event* has the sense of "something
-external just happened". 
 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, 
@@ -219,14 +222,14 @@ an event/message.
 
 ```C
 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>
 <summary>Reality check</summary>
 
 <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>
 
 </details>
@@ -283,12 +286,12 @@ Here's a more realistic version:
 
 ```C
 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
   events[nextEvent++] = evt;        // insert event into queue
   if (nextEvent == NUM_EVENTS)      // loop back to the start of queue
     nextEvent = 0;
-  enable_irq();           // end critical section, probably <100us of blockage
+  enable_irq();                     // end critical section, probably <100us of blockage
 }
 ```
 </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 
 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
 - 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
 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?
 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.
 
 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
 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,
-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
 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,
 and return from the task. Process the new state later.
-<hr>
 </details>
 
 
@@ -1870,3 +1869,31 @@ void checkRadioFreq() {
 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.
 
+### 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 \
   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_complex.dot door.dot door2.dot vending.dot
+  window_complex.dot door.dot door2.dot vending.dot up_down.dot
 
 
 OBJECTS=$(SOURCES:.md=.html)

+ 2 - 2
pandoc.css

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